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 loopx/control_plane/turn_driver/chat_queue_steering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function mutation(value: unknown): JsonObject | null {
if (value == null) return null;
const row = requireJsonObject(value, "queue input receipt");
requireNonEmptyString(row.operation_id, "queue input operation_id");
if (!["edit", "cancel"].includes(String(row.action)) ||
if ((row.action !== "edit" && row.action !== "cancel") ||
!Number.isSafeInteger(row.expected_revision) || Number(row.expected_revision) < 0 ||
row.input_revision !== Number(row.expected_revision) + 1 ||
row.action === "edit" && typeof row.message !== "string") throw new Error("Invalid queue input receipt");
Expand Down Expand Up @@ -122,7 +122,7 @@ export function planQueuedChatMutation(input: JsonObject): JsonObject {
const operationId = requireNonEmptyString(request.operation_id, "operation_id");
if (operationId.length > 160 || !/^[A-Za-z0-9._-]+$/.test(operationId) ||
!Number.isSafeInteger(request.expected_revision) || Number(request.expected_revision) < 0 ||
!["edit", "cancel"].includes(String(request.action)) ||
(request.action !== "edit" && request.action !== "cancel") ||
Object.keys(request).some(key => !["operation_id", "expected_revision", "action", "message"].includes(key))) {
throw new Error("Invalid queue input request");
}
Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.

| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
| --- | --- | --- | --- | --- |
| `queue_action_type_coercion` | A queued input action such as `["edit"]` is accepted but applies cancellation, or a stored operation accepts a non-string action. | Original JSON types, the typed planner and receipt decoder, real HTTP responses, canonical Turn/transcript/events/Session snapshots and native IO before/after refusal. | Membership checks stringify the action while effect selection and replay compare its original literal value. | Validate the original literal action at the existing owner before branching and when decoding pending or committed receipts. Cover arrays, nested arrays, objects, nulls, numbers, booleans and unknown strings; require rejection without state or native effects, then verify legal edit/cancel, original-operation replay and queue races. Preserve malformed historical evidence rather than coercing or rewriting receipts. |
| `typed_adapter_validation_scope_mismatch` | A local adapter type check passes while the same CI entry reports a large legacy dependency error set. | Exact source revisions, configured roots, cold same-environment runs, installation/import paths, and original-root plus adapter counterfactuals. | Adding runtime adapters to a kernel root list expands its import closure; an extra source path can mask that traversal locally. | Preserve the existing kernel gate and its import behavior. Make the additive adapter boundary explicit, retain imported-code debt as unqualified, and prove strict root checking with original diagnostics and core/adapter mutations in an unmasked source installation. Do not globally disable import checks, remove original roots or rewrite historical failures as passes. |
| `saved_projection_goal_route_coupling` | A saved context projection fails on conflicting default runtime roots even with native transport disabled, while the same capture remains readable. | Run the shipped projection and returned reader against one digest-bound synthetic capture, with implicit, missing and global registry selectors; retain digest/identity refusals and unchanged runtime snapshots. | Shared CLI registry discovery runs before the saved-view owner validates its source. | Classify the saved presentation at the existing CLI route owner; preserve machine-configuration routing, live authority conflict checks and source/session validation. Reuse the registry-independence integration tests rather than adding a second recovery path or repairing live state. |
| `native_reader_program_route_loss` | A native producer or read uses a selected CLI or interpreter, but returned navigation, continuation or recovery invokes another runtime. | Actual lifecycle capture, generated first-read and recovery instructions, unchanged execution of emitted commands with an isolated competing program, complete source/digest readback and installed-package provenance. | A presentation layer used its generic command-renderer default after the caller had already selected the runtime. | Pass the current program argv through existing CLI, navigation, bounded-result and reader renderers; retain constructor defaults and shell quoting. Include emitted command metadata in native byte-cost estimates. Cover cold guard and saved-context projection before direct first reads, raw/selected/index pagination, quoted aliases, refusal details and source-state immutability through real CLI and isolated wheel entrypoints. Routing preserves the selected program; it does not pin an installation version or prove Host adoption. |
Expand Down
15 changes: 15 additions & 0 deletions tests/control_plane_ts/chat_queue_steering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ test("partial input writes exclude both FIFO and steering; exact retries finish
assert.equal(planQueuedChatMutation({...f, turn: ended, request: {...request, expected_revision: 1}}).error_code, "queue_mutation_identity_conflict");
});

test("queue input actions are literal strings in requests and stored receipts", () => {
const f = facts();
const request = {action: "cancel", expected_revision: 0, operation_id: "strict-input"};
for (const action of [null, false, 0, {}, [], ["edit"], ["cancel"], [["edit"]], {toString: "edit"}, "unknown"]) {
assert.throws(() => planQueuedChatMutation({...f, request: {...request, action}}), /Invalid queue input request/);
const invalid = {...request, action, input_revision: 1};
assert.throws(() => selectQueuedChatTurns({turns: [{...f.turn, mutation: invalid}], include_reserved: true}),
/Invalid queue input receipt/);
for (const extra of [{mutation: invalid}, {operations: [invalid]}]) {
assert.throws(() => planQueuedChatMutation({...f, request, turn: {...f.turn, ...extra}}),
/Invalid queue input receipt/);
}
}
});

test("queued steering requires an unclaimed original and exact live target", () => {
const f = facts(); assert.equal(planQueuedChatSteering(f).action, "dispatch");
for (const status of ["starting", "running", "interrupting", "completed", "failed"]) {
Expand Down
15 changes: 15 additions & 0 deletions tests/presentation/test_chat_turn_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,21 @@ def test_goal_queue_input_http_with_canonical_goal_providers(ordinary, tmp_path,
qid = queued["turn_id"]
assert request(f"/api/chat/sessions/{sid}/queue")[0] == 200
path = f"/api/chat/sessions/{sid}/queue/{qid}/input"
original_turn = store.load_turn(sid, qid)
original_messages = store.messages(sid)
original_events = store.events_after(sid, qid, None)
original_session = store.load_session(sid)
for index, action in enumerate([
None, False, 0, {}, [], ["edit"], ["cancel"], [["edit"]], {"toString": "edit"}, "unknown",
]):
invalid = {"action": action, "operation_id": f"invalid-{index}", "expected_revision": 0}
response = request(path, invalid)
assert store.load_turn(sid, qid) == original_turn, response
assert store.messages(sid) == original_messages
assert store.events_after(sid, qid, None) == original_events
assert store.load_session(sid) == original_session
assert not capture.exists()
assert response[0] == 409, response
assert request(path, queue_input_body())[0] == 200
history = request(f"/api/chat/sessions/{sid}?turn_id={qid}&message_limit=200")[1]["messages"]
assert history[0]["text"] == "Corrected queued input." and history[0]["queue_input_revision"] == 1
Expand Down
Loading