Bound generation MTP pull to current objects
This commit is contained in:
parent
f7b556cc37
commit
6f3ad45cb4
6 changed files with 180 additions and 12 deletions
|
|
@ -8,6 +8,9 @@
|
|||
ACK, and current referenced generation, while preserving every retained
|
||||
generation, tombstone, and causal proof. Transport expiry has a stable
|
||||
browser-facing code and action instead of a truncated traceback.
|
||||
The reciprocal pull now reads only current coordination objects and the
|
||||
exactly referenced Android generation instead of recursively mirroring
|
||||
retained Android and desktop history.
|
||||
|
||||
- Replaced the Web Next Session placeholder with a separate read-only Core
|
||||
projection that distinguishes AI proposals from active/pending execution
|
||||
|
|
|
|||
|
|
@ -234,6 +234,10 @@ files, prior ACKs, and causal evidence are not recursively re-uploaded. Adapter
|
|||
timeouts are persisted as the stable `transport_timeout` status instead of a
|
||||
truncated Python traceback. The browser polls through one abortable scheduler,
|
||||
orders revisions per `run_id`, and rejects stale responses from older runs.
|
||||
The reciprocal MTP read is bounded to the Android peer advertisement, current
|
||||
generation reference and consumption ACK, plus the single Android generation
|
||||
named by that reference. Historical generations and other retained evidence
|
||||
are neither traversed nor deleted during polling.
|
||||
|
||||
## Data semantics
|
||||
|
||||
|
|
|
|||
|
|
@ -809,6 +809,12 @@ rewrite any source object. A bounded adapter timeout is an ambiguous transport
|
|||
failure with stable code `transport_timeout`; it is not evidence of rollback
|
||||
and does not authorize automatic mutation replay.
|
||||
|
||||
MTP polling follows the same bounded ownership rule in the opposite direction.
|
||||
It reads the peer advertisement, current Android generation reference and
|
||||
current Android consumption ACK, then downloads only the generation directory
|
||||
named by that validated reference. It does not recursively mirror retained
|
||||
Android generations, desktop publications, or unrelated exchange history.
|
||||
|
||||
`GET /api/v1/prepared-items` is a separate read-only Web projection. It does not
|
||||
change the frozen Dashboard `next_session.available` field or any exchange
|
||||
format. It lists bounded durable desktop AI proposals and relevant execution
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ deployment-tool tests cover consistent SQLite backup and candidate inventory.
|
|||
The incident regressions also prove that a phase-owned MTP outbox excludes
|
||||
retained staging and unrelated legacy files, adapter expiry produces the stable
|
||||
`transport_timeout` code, and operational failure classes remain distinct.
|
||||
The C MTP regression keeps a malformed retained generation beside the current
|
||||
reference and proves that pull downloads only the referenced generation while
|
||||
still rejecting a truncated object inside that current generation.
|
||||
|
||||
`web_prepared_items` exercises the production SQLite projection and JSON
|
||||
serializer with a proposal and an execution draft, then proves finalized and
|
||||
|
|
|
|||
|
|
@ -253,6 +253,116 @@ static TrainlogStatus pull_folder(const TrainlogGenerationMtpIo *io,
|
|||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
|
||||
static TrainlogStatus pull_named_file(const TrainlogGenerationMtpIo *io,
|
||||
const Selection *selection,
|
||||
uint32_t parent,
|
||||
const char *name,
|
||||
const char *local,
|
||||
bool required) {
|
||||
TrainlogMtpEntry entry;
|
||||
TrainlogStatus status = one_named(io, selection, parent, name, false, &entry);
|
||||
char path[1024], temporary[1060];
|
||||
struct stat value;
|
||||
int written;
|
||||
if (status == TRAINLOG_STATUS_NOT_FOUND && !required) {
|
||||
return TRAINLOG_STATUS_OK;
|
||||
}
|
||||
if (status != TRAINLOG_STATUS_OK || entry.size_bytes > 65536U) {
|
||||
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_INVALID_ARGUMENT : status;
|
||||
}
|
||||
written = snprintf(path, sizeof(path), "%s/%s", local, name);
|
||||
if (written < 0 || (size_t)written >= sizeof(path)) {
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
}
|
||||
written = snprintf(temporary, sizeof(temporary), "%s.part.%ld", path, (long)getpid());
|
||||
if (written < 0 || (size_t)written >= sizeof(temporary)) {
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
}
|
||||
(void)unlink(temporary);
|
||||
status = io->receive(selection->bus, selection->device, entry.item_id, temporary);
|
||||
if (status == TRAINLOG_STATUS_OK &&
|
||||
(stat(temporary, &value) != 0 || value.st_size < 0 ||
|
||||
(uint64_t)value.st_size != entry.size_bytes || rename(temporary, path) != 0)) {
|
||||
status = TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
if (status != TRAINLOG_STATUS_OK) {
|
||||
(void)unlink(temporary);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static bool referenced_android_generation(const char *local, char *generation, size_t size) {
|
||||
char path[1024], buffer[65537];
|
||||
int fd, written;
|
||||
ssize_t count;
|
||||
yyjson_doc *document;
|
||||
yyjson_val *root, *relative;
|
||||
const char *value, *prefix = "android-objects/generations/";
|
||||
bool valid = false;
|
||||
written = snprintf(path, sizeof(path), "%s/android-generation-v1.json", local);
|
||||
if (written < 0 || (size_t)written >= sizeof(path) ||
|
||||
(fd = open(path, O_RDONLY | O_CLOEXEC)) < 0) {
|
||||
return false;
|
||||
}
|
||||
count = read(fd, buffer, sizeof(buffer));
|
||||
(void)close(fd);
|
||||
if (count <= 0 || (size_t)count >= sizeof(buffer)) {
|
||||
return false;
|
||||
}
|
||||
document = yyjson_read(buffer, (size_t)count, YYJSON_READ_NOFLAG);
|
||||
if (document == NULL) {
|
||||
return false;
|
||||
}
|
||||
root = yyjson_doc_get_root(document);
|
||||
relative = yyjson_obj_get(root, "relative_path");
|
||||
value = yyjson_get_str(relative);
|
||||
if (yyjson_is_obj(root) && value != NULL && strncmp(value, prefix, strlen(prefix)) == 0) {
|
||||
const char *leaf = value + strlen(prefix);
|
||||
if (safe_leaf(leaf) && strlen(leaf) < size) {
|
||||
(void)memcpy(generation, leaf, strlen(leaf) + 1U);
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
yyjson_doc_free(document);
|
||||
return valid;
|
||||
}
|
||||
|
||||
static TrainlogStatus pull_referenced_generation(const TrainlogGenerationMtpIo *io,
|
||||
const Selection *selection,
|
||||
const char *local,
|
||||
const char *generation) {
|
||||
TrainlogMtpEntry android_objects, generations, selected;
|
||||
char path[1024];
|
||||
uint64_t total = 0U;
|
||||
size_t objects = 0U;
|
||||
TrainlogStatus status;
|
||||
status = one_named(io, selection, selection->root, "android-objects", true, &android_objects);
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
status = one_named(io, selection, android_objects.item_id, "generations", true, &generations);
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
status = one_named(io, selection, generations.item_id, generation, true, &selected);
|
||||
}
|
||||
if (status != TRAINLOG_STATUS_OK ||
|
||||
snprintf(path, sizeof(path), "%s/android-objects/generations/%s", local, generation) < 0 ||
|
||||
strlen(local) + strlen("/android-objects/generations/") + strlen(generation) >= sizeof(path)) {
|
||||
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_INVALID_ARGUMENT : status;
|
||||
}
|
||||
char parent[1024];
|
||||
if (snprintf(parent, sizeof(parent), "%s/android-objects/generations", local) < 0 ||
|
||||
strlen(local) + strlen("/android-objects/generations") >= sizeof(parent) ||
|
||||
mkdir_private(local) != TRAINLOG_STATUS_OK) {
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
char objects_path[1024];
|
||||
if (snprintf(objects_path, sizeof(objects_path), "%s/android-objects", local) < 0 ||
|
||||
strlen(local) + strlen("/android-objects") >= sizeof(objects_path) ||
|
||||
mkdir_private(objects_path) != TRAINLOG_STATUS_OK || mkdir_private(parent) != TRAINLOG_STATUS_OK) {
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
return pull_folder(io, selection, selected.item_id, path, 0U, &total, &objects);
|
||||
}
|
||||
|
||||
static bool same_files(const char *left, const char *right) {
|
||||
int left_fd = open(left, O_RDONLY | O_CLOEXEC);
|
||||
int right_fd = open(right, O_RDONLY | O_CLOEXEC);
|
||||
|
|
@ -459,9 +569,8 @@ TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
|
|||
char *diagnostic,
|
||||
size_t diagnostic_size) {
|
||||
Selection selection;
|
||||
uint64_t total = 0U;
|
||||
size_t objects = 0U;
|
||||
TrainlogStatus status;
|
||||
char generation[256];
|
||||
if (io == NULL || expected == NULL || local == NULL) {
|
||||
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
||||
}
|
||||
|
|
@ -470,7 +579,28 @@ TrainlogStatus trainlog_generation_mtp_pull(const TrainlogGenerationMtpIo *io,
|
|||
status = select_peer(io, expected, local, &selection, diagnostic, diagnostic_size);
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
status = pull_folder(io, &selection, selection.root, local, 0U, &total, &objects);
|
||||
/* WHY: Documents/Trainlog retains prior generations and causal evidence.
|
||||
* Recursively mirroring that durable history makes foreground sync cost
|
||||
* grow with retention and can exhaust the run deadline before admission.
|
||||
* CONTRACT: pull only current coordination objects and the one Android
|
||||
* generation named by the current reference. Missing optional objects
|
||||
* remain a normal polling state for the Python conversation.
|
||||
* INVARIANT: no remote object, ACK, tombstone, or retained generation is
|
||||
* removed or rewritten by this bounded read. */
|
||||
status = pull_named_file(
|
||||
io, &selection, selection.root, "android-peer-v1.json", local, true);
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
status = pull_named_file(
|
||||
io, &selection, selection.root, "android-generation-v1.json", local, false);
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK) {
|
||||
status = pull_named_file(
|
||||
io, &selection, selection.root, "android-consumption-ack-v1.json", local, false);
|
||||
}
|
||||
if (status == TRAINLOG_STATUS_OK &&
|
||||
referenced_android_generation(local, generation, sizeof(generation))) {
|
||||
status = pull_referenced_generation(io, &selection, local, generation);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const
|
|||
if (!f) {
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
}
|
||||
bytes = id == 9U ? 3U : o->size;
|
||||
bytes = id == 12U ? 3U : o->size;
|
||||
if (fwrite(o->data, 1, bytes, f) != bytes) {
|
||||
fclose(f);
|
||||
return TRAINLOG_STATUS_SYSTEM_ERROR;
|
||||
|
|
@ -232,11 +232,20 @@ static int test_pull_and_manifest_last(void) {
|
|||
char *manifest;
|
||||
reset();
|
||||
CHECK(mkdtemp(root) != NULL);
|
||||
add(4, 2, true, "android-objects", NULL);
|
||||
add(5, 4, true, "generations", NULL);
|
||||
add(6, 5, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
|
||||
add(7, 6, false, "payload.json", "payload");
|
||||
add(8, 6, false, "manifest.json", "manifest");
|
||||
add(4,
|
||||
2,
|
||||
false,
|
||||
"android-generation-v1.json",
|
||||
"{\"relative_path\":\"android-objects/generations/gen_11111111-1111-4111-8111-"
|
||||
"111111111111\"}");
|
||||
add(5, 2, true, "android-objects", NULL);
|
||||
add(6, 5, true, "generations", NULL);
|
||||
add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
|
||||
add(8, 7, false, "payload.json", "payload");
|
||||
add(9, 7, false, "manifest.json", "manifest");
|
||||
add(10, 6, true, "gen_22222222-2222-4222-8222-222222222222", NULL);
|
||||
add(11, 10, false, "retained.bin", "old");
|
||||
find(11)->size = 99U;
|
||||
CHECK(trainlog_generation_mtp_pull(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
|
||||
TRAINLOG_STATUS_OK);
|
||||
|
|
@ -287,12 +296,25 @@ static int test_ambiguity_and_truncation(void) {
|
|||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
|
||||
TRAINLOG_STATUS_NOT_FOUND);
|
||||
reset();
|
||||
add(9, 2, false, "bad.bin", "abc");
|
||||
find(9)->size = 99U;
|
||||
add(4,
|
||||
2,
|
||||
false,
|
||||
"android-generation-v1.json",
|
||||
"{\"relative_path\":\"android-objects/generations/gen_11111111-1111-4111-8111-"
|
||||
"111111111111\"}");
|
||||
add(5, 2, true, "android-objects", NULL);
|
||||
add(6, 5, true, "generations", NULL);
|
||||
add(7, 6, true, "gen_11111111-1111-4111-8111-111111111111", NULL);
|
||||
add(12, 7, false, "bad.bin", "abc");
|
||||
find(12)->size = 99U;
|
||||
status = trainlog_generation_mtp_pull(
|
||||
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag));
|
||||
CHECK(status != TRAINLOG_STATUS_OK);
|
||||
CHECK(trainlog_test_join_path(path, sizeof(path), root, "bad.bin"));
|
||||
CHECK(trainlog_test_join_path(
|
||||
path,
|
||||
sizeof(path),
|
||||
root,
|
||||
"android-objects/generations/gen_11111111-1111-4111-8111-111111111111/bad.bin"));
|
||||
CHECK(access(path, F_OK) != 0);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue