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
8 changes: 4 additions & 4 deletions kits/sheets/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion kits/sheets/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"react-dom": "^18.3.1",
"react-file-icon": "^1.6.0",
"react-markdown": "^9.0.1",
"reifyui": "^0.8.1",
"reifyui": "^0.8.2",
"remark-gfm": "^4.0.0",
"xlsx": "^0.18.5"
},
Expand Down
18 changes: 12 additions & 6 deletions kits/sheets/app/src/lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,18 @@ export function validate(sheet) {
if (cell.status !== undefined && !CELL_STATUS.includes(cell.status)) {
err(`cells["${k}"].status`, `is ${JSON.stringify(cell.status)}`, `Use one of: ${CELL_STATUS.join(' ')}.`);
}
// A cell that was never dispatched belongs to a run without having a session — that is
// what `skipped` means, and it is written by the app itself. Only a cell that claims to
// have RUN needs both halves of the reference.
const ran = ['running', 'done', 'failed'].includes(cell.status);
if (ran && Boolean(cell.run_id) !== Boolean(cell.session_id)) {
err(`cells["${k}"]`, 'claims to have run but has only one of run_id / session_id',
// The reference has two halves and the app writes them at different moments: run_id the
// instant a cell is dispatched, session_id when the service accepts the turn and names its
// session. So a `running` cell without a session is a cell whose turn is being started, a
// `failed` one without a session is a dispatch that never reached one, and a `skipped` one
// never had one. What is invented is a session with no run behind it, or a `done` cell
// missing either half. The old rule ("ran ⇒ both") flagged the app's own dispatch marker
// for a second or two at every batch start, as a red banner over a healthy run.
if (cell.session_id && !cell.run_id) {
err(`cells["${k}"]`, 'names a session_id without the run_id that produced it',
'Both come from a real run. Delete them, or leave the cell out entirely.');
} else if (cell.status === 'done' && (!cell.run_id || !cell.session_id)) {
err(`cells["${k}"]`, 'is done but has only one of run_id / session_id',
'Both come from a real run. Delete them, or leave the cell out entirely.');
}
}
Expand Down
14 changes: 13 additions & 1 deletion kits/sheets/app/src/lib/model.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,22 @@ test('run state in a plain cell is refused — it claims a run that never happen
assert.match(e.what, /session_id/);
});

test('an agent cell that RAN needs both run_id and session_id; a skipped one does not', () => {
test('a done cell needs both run_id and session_id; a session without a run is invented', () => {
const cols = [col('c1', 'A'), agent('c2', 'B', 'do {{A}}')];
const half = sheet(cols, [{ id: 'row_1' }], { 'row_1:c2': { run_id: 'run_1', status: 'done' } });
assert.ok(validate(half).errors.some((e) => e.what.includes('only one of run_id')));
const orphan = sheet(cols, [{ id: 'row_1' }], { 'row_1:c2': { session_id: 'hsess_1', status: 'done' } });
assert.ok(validate(orphan).errors.some((e) => e.what.includes('without the run_id')));

// The app's own lifecycle: run_id lands at dispatch, session_id when the turn is accepted, so
// a running cell is briefly a run with no session; a dispatch refused before any session
// leaves a failed cell the same way. Neither is invented, and neither may show as an error
// over a healthy run (the red banner at every batch start, 2026-09-22).
const starting = sheet(cols, [{ id: 'row_1' }], { 'row_1:c2': { run_id: 'run_1', status: 'running' } });
assert.equal(validate(starting).errors.length, 0);
const refused = sheet(cols, [{ id: 'row_1' }],
{ 'row_1:c2': { run_id: 'run_1', status: 'failed', error: 'The turn could not be started.' } });
assert.equal(validate(refused).errors.length, 0);

// The app writes exactly this for a cell it never dispatched. Flagging it made the sheet page
// show the person an error about its own correct output.
Expand Down
2 changes: 1 addition & 1 deletion kits/sheets/plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "harnessrouter-sheets",
"version": "1.0.2",
"version": "1.0.3",
"description": "A spreadsheet where a column can be an agent.",
"author": {
"name": "HarnessRouter",
Expand Down
15 changes: 10 additions & 5 deletions kits/sheets/plugin/skills/sheet-design/validate_sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,16 @@ def check_cells(sheet: dict, columns: list, row_ids: set) -> None:
st = cell.get("status")
if st is not None and st not in STATUSES:
err(f"{at}.status", f"is {json.dumps(st)}", f"use one of: {' '.join(sorted(STATUSES))}")
# A `skipped` cell belongs to a run without ever having had a session; that is what
# skipped means, and the app writes it. Only a cell claiming to have RUN needs both.
if st in ("running", "done", "failed") \
and bool(cell.get("run_id")) != bool(cell.get("session_id")):
err(at, "claims to have run but has only one of run_id / session_id",
# The reference has two halves the app writes at different moments: run_id at
# dispatch, session_id when the service accepts the turn. A running cell without a
# session is being started, a failed one without a session never reached one, a
# skipped one never had one. Invented is a session with no run, or a done cell
# missing either half. (Mirrors the app's own validator, lib/model.js.)
if cell.get("session_id") and not cell.get("run_id"):
err(at, "names a session_id without the run_id that produced it",
"both come from a real run — delete them, or leave the cell out entirely")
elif st == "done" and not (cell.get("run_id") and cell.get("session_id")):
err(at, "is done but has only one of run_id / session_id",
"both come from a real run — delete them, or leave the cell out entirely")

if col.get("type") == "checkbox" and "value" in cell and not isinstance(cell["value"], bool):
Expand Down
Loading