toolroll 0.9.84 → 0.9.85
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/CHANGELOG.md +42 -0
- package/README.md +94 -1193
- package/dist/agent-fence.d.ts +3 -3
- package/dist/agent-fence.js +3 -3
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +5 -0
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +35 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +2 -1
- package/dist/contracts/lead-tools.d.ts +30 -0
- package/dist/contracts/lead-tools.js +3 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +30 -4
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/operate.d.ts +1 -2
- package/dist/operate.js +11 -15
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/server/console.css +12 -0
- package/dist/server/pages.js +11 -1
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +2 -2
- package/dist/server/render-pages.js +7 -2
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-tasks.d.ts +1 -0
- package/dist/server/render-tasks.js +15 -5
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +18 -1
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.js +42 -6
- package/dist/store.d.ts +46 -3
- package/dist/store.js +165 -6
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +4 -0
- package/dist/work-index.js +32 -0
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
package/dist/store.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { verifyResultImage } from "./chat-evidence.js";
|
|
2
|
+
import { parseFeedbackImage } from "./result-review.js";
|
|
1
3
|
import { mintSecret } from "./secret-mint.js";
|
|
2
4
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
3
5
|
import { checkAccessOf, widerThan } from "./check-access.js";
|
|
@@ -107,6 +109,8 @@ import { configBase, envValue, namedPath } from "./names.js";
|
|
|
107
109
|
import { PULL_REQUEST_SCHEMA } from "./pull-request-schema.js";
|
|
108
110
|
import { chatTurnAmountOf, combineReportedModels, providerTurnsOf, reportedCostUsdOf, reportedModelOf, runAmountOf } from "./reported-usage.js";
|
|
109
111
|
import { usageOf } from "./run-efficiency.js";
|
|
112
|
+
import { YOUIYOUX_FILED_VIA } from "./contracts/youiyoux-request.js";
|
|
113
|
+
import { shownTermsDigest } from "./chat-terms.js";
|
|
110
114
|
// PR #263 follow-up: the built-ins the Store money seal uses, captured as this module is evaluated, so code that later
|
|
111
115
|
// replaces `Object.defineProperty` and the like can't fool the seal or the lock (lockStoreMoneyMethods refuses when
|
|
112
116
|
// they've been replaced). The four sealed method references are recorded once, at the seal, in SEALED_MONEY_METHODS.
|
|
@@ -717,6 +721,60 @@ export const V141_ADDED_COLUMNS = Object.freeze({
|
|
|
717
721
|
},
|
|
718
722
|
chat_turn: { billing_reported: { definition: "TEXT CHECK (billing_reported IS NULL OR billing_reported IN ('subscription','api-key'))", saved: "NULL" } },
|
|
719
723
|
});
|
|
724
|
+
/** v142 (youiyoux-intake.ts): youiyoux change requests as tasks. `youiyoux_intake` is one row per project the intake
|
|
725
|
+
* has looked at: a person's on/off choice (`enabled` NULL: automatic, on while the project uses youiyoux) and the last
|
|
726
|
+
* pass's answer (whether the project uses youiyoux, how many request files couldn't be read, and the problem, if any).
|
|
727
|
+
* `youiyoux_request` is one row per request taken, keyed by project and request id: the task it became (NULL: left
|
|
728
|
+
* out, `note` says why), never filed again, the status last written beside it, with the console address its links
|
|
729
|
+
* named ('' for none), and where the person pointed (route, component, file:line), cleaned, for the task page. An
|
|
730
|
+
* options request (`kind`) also keeps the options file its agent writes (`proposal`, p-<its hex>), set when filed and
|
|
731
|
+
* never read from the request again: whether it went to the project's main checkout from which result (`proposal_state`,
|
|
732
|
+
* `proposal_run`, and why not in `proposal_note`; `taken`: a file was already there, so it was left and is never read
|
|
733
|
+
* for a decision), the digest of what was copied apart from what a decision changes (`proposal_digest`), and what a
|
|
734
|
+
* person decided on it in youiyoux's hub (`decided`, `decided_note`: what Toolroll did about it). New tables only. */
|
|
735
|
+
export const V142_SCHEMA = `
|
|
736
|
+
CREATE TABLE IF NOT EXISTS youiyoux_intake (
|
|
737
|
+
repo TEXT PRIMARY KEY,
|
|
738
|
+
enabled INTEGER CHECK (enabled IS NULL OR enabled IN (0, 1)),
|
|
739
|
+
uses INTEGER NOT NULL DEFAULT 0 CHECK (uses IN (0, 1)),
|
|
740
|
+
changed_by TEXT,
|
|
741
|
+
changed_at TEXT,
|
|
742
|
+
checked_at TEXT,
|
|
743
|
+
invalid INTEGER NOT NULL DEFAULT 0 CHECK (invalid >= 0),
|
|
744
|
+
problem TEXT
|
|
745
|
+
);
|
|
746
|
+
CREATE TABLE IF NOT EXISTS youiyoux_request (
|
|
747
|
+
repo TEXT NOT NULL,
|
|
748
|
+
request TEXT NOT NULL CHECK (length(request) = 12 AND request GLOB 'r-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'),
|
|
749
|
+
task_id TEXT,
|
|
750
|
+
note TEXT,
|
|
751
|
+
filed_at TEXT NOT NULL,
|
|
752
|
+
status TEXT CHECK (status IS NULL OR status IN ('queued','needs-you','building','ready','done','failed','stopped')),
|
|
753
|
+
detail TEXT,
|
|
754
|
+
status_at TEXT,
|
|
755
|
+
written_at TEXT,
|
|
756
|
+
written_base TEXT,
|
|
757
|
+
write_problem TEXT,
|
|
758
|
+
route TEXT,
|
|
759
|
+
component TEXT,
|
|
760
|
+
source_at TEXT,
|
|
761
|
+
kind TEXT NOT NULL DEFAULT 'change' CHECK (kind IN ('change','options')),
|
|
762
|
+
proposal TEXT CHECK (proposal IS NULL OR (length(proposal) = 12 AND proposal GLOB 'p-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]')),
|
|
763
|
+
proposal_state TEXT CHECK (proposal_state IS NULL OR proposal_state IN ('copied','taken','missing','unsafe','retry')),
|
|
764
|
+
proposal_digest TEXT CHECK (proposal_digest IS NULL OR (length(proposal_digest) = 64 AND proposal_digest NOT GLOB '*[^0-9a-f]*')),
|
|
765
|
+
proposal_run INTEGER,
|
|
766
|
+
proposal_note TEXT,
|
|
767
|
+
proposal_at TEXT,
|
|
768
|
+
decided TEXT CHECK (decided IS NULL OR decided IN ('chosen','dismissed')),
|
|
769
|
+
decided_at TEXT,
|
|
770
|
+
decided_note TEXT,
|
|
771
|
+
PRIMARY KEY (repo, request),
|
|
772
|
+
UNIQUE (task_id),
|
|
773
|
+
CHECK ((kind = 'options') = (proposal IS NOT NULL)),
|
|
774
|
+
CHECK (proposal_state IS NULL OR kind = 'options'),
|
|
775
|
+
CHECK (decided IS NULL OR coalesce(proposal_state = 'copied' AND proposal_digest IS NOT NULL, 0))
|
|
776
|
+
);
|
|
777
|
+
`;
|
|
720
778
|
/** A project's "Show commands in the live view" (live-detail.ts): absent means off. */
|
|
721
779
|
export const LIVE_DETAIL_SCHEMA = `
|
|
722
780
|
CREATE TABLE IF NOT EXISTS live_detail_setting (
|
|
@@ -1030,7 +1088,11 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
1030
1088
|
// v141 adds, and changes no saved row: the eval set's records (docs/plans/evals-nightly.md, part C) — campaigns,
|
|
1031
1089
|
// attempts, each attempt's provider dispatches, the baseline pin and eval debt (V141_SCHEMA) — and how a run's or chat
|
|
1032
1090
|
// request's billing was known (V141_ADDED_COLUMNS; saved rows NULL: unknown).
|
|
1033
|
-
|
|
1091
|
+
// v142 adds, and changes no saved row: youiyoux change requests as tasks (V142_SCHEMA, new tables only) — each
|
|
1092
|
+
// project's intake choice and last answer, and each request taken, with the task it became.
|
|
1093
|
+
// v143 adds, and changes no saved row: optional exact-screenshot context on result feedback
|
|
1094
|
+
// (diff_comment.image_json, V143_ADDED_COLUMNS; saved notes read NULL and keep their file/line target).
|
|
1095
|
+
export const SCHEMA_VERSION = 143;
|
|
1034
1096
|
/** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
|
|
1035
1097
|
export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
|
|
1036
1098
|
/** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
|
|
@@ -1163,7 +1225,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
|
|
|
1163
1225
|
* unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
|
|
1164
1226
|
* (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
|
|
1165
1227
|
*/
|
|
1166
|
-
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141]);
|
|
1228
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143]);
|
|
1167
1229
|
/** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
|
|
1168
1230
|
export const V119_ADDED_COLUMNS = Object.freeze({
|
|
1169
1231
|
task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
|
|
@@ -1299,6 +1361,10 @@ export const V140_ADDED_COLUMNS = Object.freeze({
|
|
|
1299
1361
|
reported_cost_usd: { definition: "REAL CHECK (reported_cost_usd IS NULL OR reported_cost_usd >= 0)", saved: "NULL" },
|
|
1300
1362
|
},
|
|
1301
1363
|
});
|
|
1364
|
+
/** v143: optional screenshot context on result feedback; existing feedback stays unchanged (saved rows read NULL). */
|
|
1365
|
+
export const V143_ADDED_COLUMNS = Object.freeze({
|
|
1366
|
+
diff_comment: { image_json: { definition: "TEXT", saved: "NULL" } },
|
|
1367
|
+
});
|
|
1302
1368
|
/**
|
|
1303
1369
|
* v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
|
|
1304
1370
|
* release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
|
|
@@ -4549,6 +4615,13 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
|
|
|
4549
4615
|
for (const [column, { definition }] of Object.entries(columns))
|
|
4550
4616
|
addColumn(db, table, column, definition);
|
|
4551
4617
|
}
|
|
4618
|
+
// v142: youiyoux change requests as tasks (new tables only).
|
|
4619
|
+
db.exec(V142_SCHEMA);
|
|
4620
|
+
// v143: optional exact-screenshot context on result feedback (V143_ADDED_COLUMNS: saved notes read NULL).
|
|
4621
|
+
for (const [table, columns] of Object.entries(V143_ADDED_COLUMNS)) {
|
|
4622
|
+
for (const [column, { definition }] of Object.entries(columns))
|
|
4623
|
+
addColumn(db, table, column, definition);
|
|
4624
|
+
}
|
|
4552
4625
|
// Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
|
|
4553
4626
|
// nothing to reclaim.
|
|
4554
4627
|
if (!fresh)
|
|
@@ -7282,6 +7355,14 @@ export class Store {
|
|
|
7282
7355
|
.get(taskId);
|
|
7283
7356
|
if (writtenByLead !== undefined)
|
|
7284
7357
|
return false;
|
|
7358
|
+
// THE OUTSIDE-REQUEST QUARANTINE (youiyoux-intake.ts): a task filed from a youiyoux change request is text
|
|
7359
|
+
// anyone who can write the project's folder wrote, so only a person who read every word of it approves it.
|
|
7360
|
+
// Its provenance is immutable, so no road around this exists. A mode, a grant, a schedule, auto-check and
|
|
7361
|
+
// auto-green (approvals no person read) never seal it. A person's own ceremony does. So does their paired-
|
|
7362
|
+
// chat tap, only with the digest of every word the card showed (`terms`, chat-terms.ts), recomputed below
|
|
7363
|
+
// from the live scope: a card that cut a word, or terms that changed since, seal nothing.
|
|
7364
|
+
if (basis.kind !== "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA)
|
|
7365
|
+
return false;
|
|
7285
7366
|
// CHECK ACCESS (v132, check-access.ts): a scope whose checks reach beyond the fence (sign-ins, or no sandbox at
|
|
7286
7367
|
// all) seals only on a person's password ceremony — never a mode, a grant, a chat confirmation or a carry.
|
|
7287
7368
|
const profileText = this.db.prepare("SELECT profile_json FROM task_scope WHERE task_id = ?").get(taskId)?.["profile_json"];
|
|
@@ -7348,6 +7429,10 @@ export class Store {
|
|
|
7348
7429
|
return false;
|
|
7349
7430
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7350
7431
|
return false;
|
|
7432
|
+
// The outside-request quarantine's chat road (above): the card showed every word of these live terms.
|
|
7433
|
+
if (basis?.kind === "chat" && this.filedViaOf(taskId) === YOUIYOUX_FILED_VIA
|
|
7434
|
+
&& !(basis.card === "terms" && typeof basis.cardDigest === "string" && basis.cardDigest === shownTermsDigest(readScope(working))))
|
|
7435
|
+
return false;
|
|
7351
7436
|
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
7352
7437
|
// in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
|
|
7353
7438
|
const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
|
|
@@ -9535,13 +9620,21 @@ export class Store {
|
|
|
9535
9620
|
if (artifact === undefined || artifact.kind !== "terminal-diff") {
|
|
9536
9621
|
throw new Error(`artifact ${args.artifactId} is not run ${args.runId}'s terminal diff — comments bind to the exact bytes reviewed`);
|
|
9537
9622
|
}
|
|
9623
|
+
const image = parseFeedbackImage(args.image);
|
|
9624
|
+
if (image === false)
|
|
9625
|
+
throw new Error("Invalid feedback image.");
|
|
9626
|
+
if (image !== null) {
|
|
9627
|
+
const shot = this.artifactForRun(args.runId, image.artifact);
|
|
9628
|
+
if (image.run !== args.runId || shot?.kind !== "screenshot" || shot.sha256 !== image.sha256)
|
|
9629
|
+
throw new Error("The feedback image does not belong to this result.");
|
|
9630
|
+
}
|
|
9538
9631
|
// The unique index is the real concurrent guard (audit C-9): a race on
|
|
9539
9632
|
// the same source key lands in DO NOTHING, not in a thrown constraint.
|
|
9540
9633
|
const inserted = this.db
|
|
9541
|
-
.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, source_key)
|
|
9542
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9634
|
+
.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, source_key, image_json)
|
|
9635
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9543
9636
|
ON CONFLICT (source_key) WHERE source_key IS NOT NULL DO NOTHING`)
|
|
9544
|
-
.run(args.artifactId, artifact.sha256, args.runId, args.path, args.line, args.note, args.author, now.toISOString(), args.sourceKey ?? null);
|
|
9637
|
+
.run(args.artifactId, artifact.sha256, args.runId, args.path, args.line, args.note, args.author, now.toISOString(), args.sourceKey ?? null, image === null ? null : JSON.stringify(image));
|
|
9545
9638
|
return Number(inserted.changes) === 0 ? null : Number(inserted.lastInsertRowid);
|
|
9546
9639
|
}
|
|
9547
9640
|
/** Live (unsuperseded, unconsumed) comments on a run's terminal diff. */
|
|
@@ -10229,6 +10322,19 @@ export class Store {
|
|
|
10229
10322
|
const row = this.db.prepare("SELECT flow, card, card_entry, source_json FROM task_source WHERE task_id = ?").get(taskId);
|
|
10230
10323
|
return row === undefined ? null : { flow: Number(row["flow"]), card: Number(row["card"]), entry: Number(row["card_entry"]), sourceJson: String(row["source_json"]) };
|
|
10231
10324
|
}
|
|
10325
|
+
/** v142: where a task filed from a youiyoux change or options request came from, as kept when it was filed
|
|
10326
|
+
* (youiyoux-intake.ts; a revision reads its first version's): the request, its kind, and where the person pointed. Null for every other task. */
|
|
10327
|
+
youiyouxOriginOf(taskId) {
|
|
10328
|
+
const text = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
10329
|
+
let id = taskId;
|
|
10330
|
+
for (let hop = 0; id !== null && hop < 20; hop++) {
|
|
10331
|
+
const row = this.db.prepare("SELECT request, kind, route, component, source_at FROM youiyoux_request WHERE task_id = ?").get(id);
|
|
10332
|
+
if (row !== undefined)
|
|
10333
|
+
return { request: String(row["request"]), kind: row["kind"] === "options" ? "options" : "change", route: text(row["route"]), component: text(row["component"]), at: text(row["source_at"]) };
|
|
10334
|
+
id = this.lookupRef(id)?.revisionOf ?? null;
|
|
10335
|
+
}
|
|
10336
|
+
return null;
|
|
10337
|
+
}
|
|
10232
10338
|
// ---- the revision boundary: ONE field-by-field policy -------------------
|
|
10233
10339
|
/**
|
|
10234
10340
|
* Seal a revision in ONE transaction (Codex M5-M8 audit, C-3; contract
|
|
@@ -10320,7 +10426,14 @@ export class Store {
|
|
|
10320
10426
|
if (ids.size !== args.commentIds.length || live.length !== ids.size) {
|
|
10321
10427
|
throw new RevisionRaced("this annotation batch is no longer whole — read the current comments before sealing it");
|
|
10322
10428
|
}
|
|
10323
|
-
const
|
|
10429
|
+
for (const one of live) {
|
|
10430
|
+
if (one.image == null)
|
|
10431
|
+
continue;
|
|
10432
|
+
const image = sourceRef.repo === null ? null : verifyResultImage(this, args.brief.evidenceRoot, [sourceRef.repo], { taskId: args.source.task, ...one.image });
|
|
10433
|
+
if (one.image.run !== sourceRun.id || image === null || !image.ok)
|
|
10434
|
+
return refuse("brief-custody", "A feedback screenshot is unavailable or changed. The notes were not consumed.");
|
|
10435
|
+
}
|
|
10436
|
+
const exactComment = (one) => ({ id: one["id"], path: one["path"], line: one["line"], note: one["note"], author: one["author"], createdAt: one["createdAt"], image: one["image"] ?? null });
|
|
10324
10437
|
const expected = live.map(one => exactComment({ ...one }));
|
|
10325
10438
|
const described = Array.isArray(named.comments) && named.comments.every(one => one !== null && typeof one === "object" && !Array.isArray(one))
|
|
10326
10439
|
? named.comments.map(one => exactComment(one)) : null;
|
|
@@ -20632,6 +20745,7 @@ function readDiffComment(row) {
|
|
|
20632
20745
|
id: Number(row["id"]),
|
|
20633
20746
|
artifact: Number(row["artifact"]),
|
|
20634
20747
|
artifactSha: String(row["artifact_sha"]),
|
|
20748
|
+
image: readFeedbackImage(row["image_json"]),
|
|
20635
20749
|
run: Number(row["run"]),
|
|
20636
20750
|
path: row["path"] === null ? null : String(row["path"]),
|
|
20637
20751
|
line: row["line"] === null ? null : Number(row["line"]),
|
|
@@ -21510,3 +21624,48 @@ export function lockStoreMoneyMethods(store, prototype = Store.prototype) {
|
|
|
21510
21624
|
throw new Error(`Store.${name} couldn't be locked`);
|
|
21511
21625
|
}
|
|
21512
21626
|
}
|
|
21627
|
+
/** A saved note's screenshot reference (v143), or null; a damaged one refuses rather than reading as none. */
|
|
21628
|
+
function readFeedbackImage(value) {
|
|
21629
|
+
const image = parseFeedbackImage(value);
|
|
21630
|
+
if (image === false)
|
|
21631
|
+
throw new Error("The saved feedback image is damaged.");
|
|
21632
|
+
return image;
|
|
21633
|
+
}
|
|
21634
|
+
/** Takes a saved note's screenshot off without losing the note or its history (v143). Fail closed, in one write
|
|
21635
|
+
* transaction: the note must be live and unconsumed on this run, its screenshot must be genuinely unusable right now
|
|
21636
|
+
* (`unusable` is result-actions.ts feedbackImageProblem, re-read inside the transaction, never the page's say-so), and
|
|
21637
|
+
* the change is recorded in the action ledger under the person who made it. The note is superseded by an exact
|
|
21638
|
+
* imageless copy; the original stays in history. The caller has already proved the person may request changes on
|
|
21639
|
+
* this run (the same authority that seals any saved note into a revision); that standing is re-proved here too. */
|
|
21640
|
+
export function dropFeedbackImage(store, runId, commentId, actor, unusable, now) {
|
|
21641
|
+
return store.transact(() => {
|
|
21642
|
+
// The person's standing is re-read under the write lock: a revocation, a downgrade or a lost project committed
|
|
21643
|
+
// after the request was admitted refuses here (check and write together).
|
|
21644
|
+
const run = store.getRun(runId);
|
|
21645
|
+
const repo = run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
21646
|
+
const account = store.accountOf(actor);
|
|
21647
|
+
if (run === null || account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(actor, repo)) {
|
|
21648
|
+
return { ok: false, reason: "not-authorized" };
|
|
21649
|
+
}
|
|
21650
|
+
const live = store.handle.prepare(`SELECT id, image_json FROM diff_comment WHERE id = ? AND run = ? AND superseded_by IS NULL AND consumed_by IS NULL
|
|
21651
|
+
AND reviewer_run IS NULL AND image_json IS NOT NULL`).get(commentId, runId);
|
|
21652
|
+
if (live === undefined)
|
|
21653
|
+
return { ok: false, reason: "not-found" };
|
|
21654
|
+
const image = readFeedbackImage(live["image_json"]);
|
|
21655
|
+
if (image === null)
|
|
21656
|
+
return { ok: false, reason: "not-found" };
|
|
21657
|
+
const problem = unusable(image);
|
|
21658
|
+
if (problem === null)
|
|
21659
|
+
return { ok: false, reason: "image-usable" };
|
|
21660
|
+
const copy = store.handle.prepare(`INSERT INTO diff_comment (artifact, artifact_sha, run, path, line, note, author, created_at, reviewer_run, severity)
|
|
21661
|
+
SELECT artifact, artifact_sha, run, path, line, note, author, ?, reviewer_run, severity FROM diff_comment WHERE id = ?`).run(now.toISOString(), commentId);
|
|
21662
|
+
const id = Number(copy.lastInsertRowid);
|
|
21663
|
+
const moved = store.handle.prepare("UPDATE diff_comment SET superseded_by = ? WHERE id = ? AND superseded_by IS NULL AND consumed_by IS NULL").run(id, commentId);
|
|
21664
|
+
if (Number(moved.changes) !== 1)
|
|
21665
|
+
throw new Error("The saved note changed while its screenshot was being removed.");
|
|
21666
|
+
store.recordAction({ at: now.toISOString(), actor, repo,
|
|
21667
|
+
taskId: store.externalIdFor(run.taskRef) ?? null, runId, action: "feedback screenshot removed", outcome: "recorded", source: "work",
|
|
21668
|
+
detail: `note ${commentId} kept as note ${id} without screenshot ${image.artifact}: ${problem}` });
|
|
21669
|
+
return { ok: true, id };
|
|
21670
|
+
});
|
|
21671
|
+
}
|
package/dist/task-source.d.ts
CHANGED
|
@@ -51,8 +51,20 @@ export type TaskOrigin = {
|
|
|
51
51
|
url: string | null;
|
|
52
52
|
host: string | null;
|
|
53
53
|
} | null;
|
|
54
|
+
/** A youiyoux change or options request (youiyoux-intake.ts): its id, its kind, and where the person pointed. */
|
|
55
|
+
request?: {
|
|
56
|
+
id: string;
|
|
57
|
+
kind: "change" | "options";
|
|
58
|
+
route: string | null;
|
|
59
|
+
component: string | null;
|
|
60
|
+
at: string | null;
|
|
61
|
+
} | null;
|
|
54
62
|
};
|
|
55
|
-
/** A task's origin, or null when it came from neither a flow
|
|
63
|
+
/** A task's origin, or null when it came from neither a flow, an outside event nor a youiyoux request. */
|
|
56
64
|
export declare function originOf(store: Store, taskId: string, canSee: (repo: string) => boolean): TaskOrigin | null;
|
|
65
|
+
/** A youiyoux request by its kind: "youiyoux change request r-…" or "youiyoux options request r-…". */
|
|
66
|
+
export declare function requestName(request: NonNullable<TaskOrigin["request"]>): string;
|
|
67
|
+
/** Where a youiyoux request pointed, in words: "Button on /flows (src/button.tsx:12)". */
|
|
68
|
+
export declare function requestWords(request: NonNullable<TaskOrigin["request"]>): string;
|
|
57
69
|
/** The one line an approval shows about an outside event that started its work: context, never a term agreed to. */
|
|
58
70
|
export declare function originNote(origin: TaskOrigin | null | undefined): string | null;
|
package/dist/task-source.js
CHANGED
|
@@ -71,8 +71,11 @@ export function cardReference(source) {
|
|
|
71
71
|
const snapshot = snapshotOf(source ?? null);
|
|
72
72
|
return snapshot === null ? "" : referenceWords(snapshot);
|
|
73
73
|
}
|
|
74
|
-
/** A task's origin, or null when it came from neither a flow
|
|
74
|
+
/** A task's origin, or null when it came from neither a flow, an outside event nor a youiyoux request. */
|
|
75
75
|
export function originOf(store, taskId, canSee) {
|
|
76
|
+
const request = store.youiyouxOriginOf(taskId);
|
|
77
|
+
if (request !== null)
|
|
78
|
+
return { flow: null, source: null, request: { id: request.request, kind: request.kind, route: request.route, component: request.component, at: request.at } };
|
|
76
79
|
const kept = store.taskSourceOf(taskId);
|
|
77
80
|
const flowId = kept?.flow ?? Number(/^flow:(\d+)$/.exec(store.filedViaOf(taskId) ?? "")?.[1] ?? NaN);
|
|
78
81
|
const snapshot = kept === null ? null : readSnapshot(kept.sourceJson);
|
|
@@ -84,8 +87,21 @@ export function originOf(store, taskId, canSee) {
|
|
|
84
87
|
source: snapshot === null ? null : { label: snapshot.label, proved: provedWords(snapshot), url: snapshot.external.url, host: hostOf(snapshot.external.url) },
|
|
85
88
|
};
|
|
86
89
|
}
|
|
90
|
+
/** A youiyoux request by its kind: "youiyoux change request r-…" or "youiyoux options request r-…". */
|
|
91
|
+
export function requestName(request) {
|
|
92
|
+
return `youiyoux ${request.kind === "options" ? "options" : "change"} request ${request.id}`;
|
|
93
|
+
}
|
|
94
|
+
/** Where a youiyoux request pointed, in words: "Button on /flows (src/button.tsx:12)". */
|
|
95
|
+
export function requestWords(request) {
|
|
96
|
+
const what = [request.component, request.route === null ? null : `on ${request.route}`].filter(one => one !== null).join(" ");
|
|
97
|
+
return `${what === "" ? "" : what}${request.at === null ? "" : `${what === "" ? "" : " "}(${request.at})`}`;
|
|
98
|
+
}
|
|
87
99
|
/** The one line an approval shows about an outside event that started its work: context, never a term agreed to. */
|
|
88
100
|
export function originNote(origin) {
|
|
101
|
+
if (origin?.request != null) {
|
|
102
|
+
const where = requestWords(origin.request);
|
|
103
|
+
return `From ${requestName(origin.request)}${where === "" ? "" : `: ${where}`}. Whoever can write the project's folder wrote these words; read them all before you approve.`;
|
|
104
|
+
}
|
|
89
105
|
if (origin?.source == null)
|
|
90
106
|
return null;
|
|
91
107
|
const where = origin.source.host === null ? "" : ` · ${origin.source.host}`;
|
package/dist/telegram-lead.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CHAT_ACTIONS, sharedActionAllowsChallenge, sharedActionPayload } from "./chat-actions.js";
|
|
2
|
-
import { taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, NO_PHONE_LINK, NO_TASK_LINK,
|
|
2
|
+
import { taskInCeiling, mirrorToTaskChat, couldNotAnswerText, channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, NO_PHONE_LINK, NO_TASK_LINK, phoneProposalLink, confirmedLink, phoneProposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText } from "./chat-channel.js";
|
|
3
3
|
export { channelRepos as telegramConversationRepos, resolveChannelLead as resolveTelegramLead, tooLongText, whichTaskText, replyContextFor, NO_PHONE_LINK, NO_TASK_LINK, proposalLink, confirmedLink, proposalPreview, proposalOutcomeText, handoffCardText, confirmedCardText, CHAT_ACTION_PARITY as TELEGRAM_ACTION_PARITY, parityGaps } from "./chat-channel.js";
|
|
4
4
|
/**
|
|
5
5
|
* The paired phone as one more way to use the same assistant.
|
|
@@ -207,7 +207,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
207
207
|
return { effects, confirmed: false, ignored: true };
|
|
208
208
|
}
|
|
209
209
|
const who = verified.who;
|
|
210
|
-
const preview =
|
|
210
|
+
const preview = phoneProposalPreview(store, proposal, actionRepos);
|
|
211
211
|
const origin = options.phoneOrigin?.() ?? null;
|
|
212
212
|
if (preview.closed) {
|
|
213
213
|
store.consumeTelegramProposalActions(proposal.id, now);
|
|
@@ -239,7 +239,7 @@ export function applyProposalTap(store, binding, token, message, repos, options,
|
|
|
239
239
|
// A forged keyboard on a handoff card: nothing acts; the card is
|
|
240
240
|
// repainted with its current link (or the honest absence of one).
|
|
241
241
|
store.consumeTelegramProposalActions(proposal.id, now);
|
|
242
|
-
const wanted =
|
|
242
|
+
const wanted = phoneProposalLink(store, proposal, actionRepos, "telegram");
|
|
243
243
|
const link = phoneLinkButton(origin, wanted);
|
|
244
244
|
ack(link === null ? "this step finishes on the computer" : "open the button below to finish this step");
|
|
245
245
|
edit(handoffCardText(preview.text, linkNote(origin, wanted)), keyboardWith(null, link));
|
|
@@ -545,7 +545,7 @@ async function runTelegramConversation(row, args) {
|
|
|
545
545
|
// or went away carries the current truth, never a stored URL.
|
|
546
546
|
const proposal = part.kind === "card" && part.proposal !== null ? store.getLeadProposal(part.proposal) : null;
|
|
547
547
|
const origin = proposal === null ? null : options.phoneOrigin?.() ?? null;
|
|
548
|
-
const wanted = proposal === null ? null :
|
|
548
|
+
const wanted = proposal === null ? null : phoneProposalLink(store, proposal, repos);
|
|
549
549
|
const keyboard = keyboardWith(part.keyboard, phoneLinkButton(origin, wanted));
|
|
550
550
|
// The lead's own words carry their bold anchors and labelled links as entities, never as Markdown characters.
|
|
551
551
|
const voiced = part.kind === "reply" ? telegramReply(part.text) : null;
|
|
@@ -625,7 +625,7 @@ async function runTelegramConversation(row, args) {
|
|
|
625
625
|
for (const image of selected)
|
|
626
626
|
parts.push({ kind: "image", text: image.caption, taskId: image.taskId, run: image.run, artifact: image.artifact, sha256: image.sha256 });
|
|
627
627
|
for (const proposal of proposals) {
|
|
628
|
-
const preview =
|
|
628
|
+
const preview = phoneProposalPreview(store, proposal, repos);
|
|
629
629
|
const keyboard = preview.buttons ? mintCardTokens(store, binding, proposal.id, now).keyboard : null;
|
|
630
630
|
parts.push({ kind: "card", text: preview.text, proposal: proposal.id, keyboard });
|
|
631
631
|
}
|
|
@@ -763,11 +763,13 @@ async function runTelegramConversation(row, args) {
|
|
|
763
763
|
react: () => transport("setMessageReaction", { chat_id: chatId, message_id: Number(row.messageId), reaction: [{ type: "emoji", emoji: WARM_EMOJI }] }),
|
|
764
764
|
typing: () => transport("sendChatAction", { chat_id: chatId, action: "typing" }),
|
|
765
765
|
});
|
|
766
|
+
const replyImage = row.replyTo === null ? undefined : store.telegramMessageBindings(binding, row.replyTo).find(one => one.taskId === row.taskId && one.run === row.sourceRun && one.image !== undefined)?.image;
|
|
766
767
|
let outcome;
|
|
767
768
|
try {
|
|
768
769
|
outcome = await runLeadTurn({
|
|
769
770
|
store, who, session, thread, config, key: null, message: row.text, requestId: row.request,
|
|
770
771
|
...(row.context === null ? {} : { context: row.context }),
|
|
772
|
+
...(replyImage === undefined ? {} : { replyImage }),
|
|
771
773
|
...(options.subscriptionRunner === undefined ? {} : { subscriptionRunner: options.subscriptionRunner }),
|
|
772
774
|
clock, evidenceRoot: options.evidenceRoot, revalidate, mediaDelivery: "documents", channel: "telegram", onProgress: warm.onProgress,
|
|
773
775
|
});
|
package/dist/telegram.js
CHANGED
|
@@ -1852,7 +1852,7 @@ function applyConversation(context, update, effects) {
|
|
|
1852
1852
|
if (tasks.length === 1) {
|
|
1853
1853
|
taskId = tasks[0];
|
|
1854
1854
|
sourceRun = bindings.find(one => one.run !== null)?.run ?? null;
|
|
1855
|
-
context_ = replyContextFor(taskId, sourceRun);
|
|
1855
|
+
context_ = replyContextFor(taskId, sourceRun, bindings.find(one => one.image !== undefined)?.image);
|
|
1856
1856
|
}
|
|
1857
1857
|
}
|
|
1858
1858
|
// No reply target: the task this chat chose to talk about, if any. The
|
package/dist/toolroll-update.js
CHANGED
|
@@ -37,7 +37,7 @@ import { removeTerminalUpdateGate } from "./terminal-catalog.js";
|
|
|
37
37
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
|
|
38
38
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
39
39
|
import { NAME } from "./names.js";
|
|
40
|
-
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS } from "./store.js";
|
|
40
|
+
import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS } from "./store.js";
|
|
41
41
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
42
42
|
import { markNeverIndex } from "./never-index.js";
|
|
43
43
|
import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
|
|
@@ -409,7 +409,7 @@ const HISTORY_RULES = {
|
|
|
409
409
|
// v126: a receipt's state, acceptance time and held item, a trigger's credential generation and turned-away count,
|
|
410
410
|
// and who made a card read none, 0, or none. A table with an earlier rule (flow_trigger's v115 move) keeps it too.
|
|
411
411
|
// v128: a lead message's pending flag reads waiting exactly for a team queue's unadmitted operator message.
|
|
412
|
-
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS))
|
|
412
|
+
...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS, V143_ADDED_COLUMNS))
|
|
413
413
|
.map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
|
|
414
414
|
};
|
|
415
415
|
/** The tables a migration may remove whole (v115's removed features). */
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export declare function effectivePrimary(env: Record<string, string | undefined>
|
|
|
35
35
|
legacyWarning?: string;
|
|
36
36
|
};
|
|
37
37
|
export declare function loadConsoleUrl(env: Record<string, string | undefined>, dir: string): string | null;
|
|
38
|
+
/** Where `up` last served its console, kept beside the database so `onboard` names the same address (operate.ts). */
|
|
39
|
+
export declare const UP_CONSOLE_FILE = "up-console.json";
|
|
40
|
+
export declare function readUpConsoleUrl(dir: string): string | null;
|
|
41
|
+
/** This machine's own console, where `up` last served it: only a plain http address on a loopback name (127.0.0.1,
|
|
42
|
+
* localhost, ::1), never a host guessed from it; null otherwise. */
|
|
43
|
+
export declare function localConsoleUrl(dir: string): string | null;
|
|
38
44
|
/**
|
|
39
45
|
* The origin a phone link may open, or null. The SAME console-url setting
|
|
40
46
|
* chat links use — read again on every call, never cached, so a changed or
|
package/dist/webhooks.js
CHANGED
|
@@ -107,6 +107,31 @@ export function loadConsoleUrl(env, dir) {
|
|
|
107
107
|
const configured = envValue(env, "CONSOLE_URL") ?? readTrimmed(join(dir, CONSOLE_FILE));
|
|
108
108
|
return configured === undefined || configured === null || configured === "" ? null : configured.replace(/\/+$/, "");
|
|
109
109
|
}
|
|
110
|
+
/** Where `up` last served its console, kept beside the database so `onboard` names the same address (operate.ts). */
|
|
111
|
+
export const UP_CONSOLE_FILE = "up-console.json";
|
|
112
|
+
export function readUpConsoleUrl(dir) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(readFileSync(join(dir, UP_CONSOLE_FILE), "utf8"));
|
|
115
|
+
return typeof parsed.url === "string" && /^https?:\/\/[^\s]+$/.test(parsed.url) ? parsed.url.replace(/\/+$/, "") : null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** This machine's own console, where `up` last served it: only a plain http address on a loopback name (127.0.0.1,
|
|
122
|
+
* localhost, ::1), never a host guessed from it; null otherwise. */
|
|
123
|
+
export function localConsoleUrl(dir) {
|
|
124
|
+
const served = readUpConsoleUrl(dir);
|
|
125
|
+
if (served === null)
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
const url = new URL(served);
|
|
129
|
+
return url.protocol === "http:" && isLoopbackHost(url.hostname) && url.hostname !== "0.0.0.0" && url.hostname !== "[::]" ? served : null;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
110
135
|
/** A dotted IPv4 that names this machine or no machine: loopback, unspecified. */
|
|
111
136
|
function isLocalIpv4(address) {
|
|
112
137
|
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(address) || address === "0.0.0.0";
|
package/dist/work-index.d.ts
CHANGED
|
@@ -114,6 +114,10 @@ export type WorkStateRow = {
|
|
|
114
114
|
members: number[];
|
|
115
115
|
};
|
|
116
116
|
export declare function workStateRows(store: Store, now: Date, access: WorkSummaryAccess): WorkStateRow[];
|
|
117
|
+
/** Activity uses the Tasks list's exact family admission and saved completion rules.
|
|
118
|
+
* Only the requested project enters the projection. The extra row signals truncation;
|
|
119
|
+
* attempts are aggregated in SQLite, never loaded as an unbounded history. */
|
|
120
|
+
export declare function workOutcomeRows(store: Store, now: Date, access: WorkSummaryAccess, project: string, since: string, limit: number): Record<string, unknown>[];
|
|
117
121
|
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
118
122
|
* list's own rows, so a completed family (root or any revision), a failed
|
|
119
123
|
* check and a superseded version never count. */
|
package/dist/work-index.js
CHANGED
|
@@ -453,6 +453,38 @@ export function workStateRows(store, now, access) {
|
|
|
453
453
|
leadClaim: n(row, 'lead_claim') === 1, liveRun: row['live_run'] == null ? null : n(row, 'live_run'), unfinished: row['unfinished'] == null ? null : n(row, 'unfinished'),
|
|
454
454
|
resultRun: row['result_id'] == null ? null : n(row, 'result_id'), waits: Number(row['rank']) === 2, version: String(row['version']), at: String(row['ask_at'] ?? ''), askRun: row['ask_run'] == null ? null : n(row, 'ask_run'), members: JSON.parse(String(row['members'] ?? '[]')).map(Number) }));
|
|
455
455
|
}
|
|
456
|
+
/** Activity uses the Tasks list's exact family admission and saved completion rules.
|
|
457
|
+
* Only the requested project enters the projection. The extra row signals truncation;
|
|
458
|
+
* attempts are aggregated in SQLite, never loaded as an unbounded history. */
|
|
459
|
+
export function workOutcomeRows(store, now, access, project, since, limit) {
|
|
460
|
+
return store.handle.prepare(`${registerValidators(store)}, selected AS MATERIALIZED (
|
|
461
|
+
SELECT * FROM ranked WHERE code='ready-to-check' OR code='history-problem'
|
|
462
|
+
OR (code='complete' AND checked_at >= $since AND checked_at <= $now)
|
|
463
|
+
ORDER BY sort_at DESC,root_ref DESC LIMIT $outcomeLimit
|
|
464
|
+
), amounts AS (
|
|
465
|
+
-- reported-usage.ts runAmountOf, row for row: only evidenced billing on a run whose cost was reported is known;
|
|
466
|
+
-- an API run counts its priced spend as dollars (a safe integer), a subscription run its reported cost as usage,
|
|
467
|
+
-- never dollars. TOTAL sums as a float, so no family's sum can overflow.
|
|
468
|
+
SELECT m.root_ref,r.cost_usd,s.microusd,
|
|
469
|
+
CASE WHEN r.billing_basis IS NOT 'evidence' OR COALESCE(r.cost_unknown,1)<>0 THEN 'unknown'
|
|
470
|
+
WHEN s.billing='subscription' THEN 'subscription'
|
|
471
|
+
WHEN s.billing='api-key' AND typeof(s.microusd)='integer' AND s.microusd>=0 AND s.microusd<=9007199254740991 THEN 'api'
|
|
472
|
+
ELSE 'unknown' END amount
|
|
473
|
+
FROM selected f JOIN members m ON m.root_ref=f.root_ref JOIN run r ON r.task_ref=m.ref_id LEFT JOIN run_spend s ON s.run=r.id
|
|
474
|
+
), attempts AS (
|
|
475
|
+
SELECT root_ref,COUNT(*) runs,COUNT(CASE WHEN amount='api' THEN 1 END) api_runs,
|
|
476
|
+
TOTAL(CASE WHEN amount='api' THEN microusd END) api_microusd,
|
|
477
|
+
COUNT(CASE WHEN amount='subscription' THEN 1 END) subscription_runs,
|
|
478
|
+
SUM(CASE WHEN amount='subscription' AND typeof(cost_usd) IN ('integer','real') AND cost_usd>=0 THEN cost_usd END) subscription_usd
|
|
479
|
+
FROM amounts GROUP BY root_ref
|
|
480
|
+
) SELECT f.root_id,f.id task_id,f.root_title title,f.code,f.finished_at,f.result_id,f.result_role,
|
|
481
|
+
f.version_count,f.checked_at,f.checked_actor,f.checked_detail,
|
|
482
|
+
(SELECT lead FROM task_act WHERE task_ref=f.root_ref AND act='completed' AND at=f.checked_at ORDER BY id DESC LIMIT 1) completed_by_lead,
|
|
483
|
+
(SELECT status FROM run_check WHERE run=f.result_id) check_status,
|
|
484
|
+
COALESCE(a.runs,0) runs,COALESCE(a.api_runs,0) api_runs,a.api_microusd,COALESCE(a.subscription_runs,0) subscription_runs,a.subscription_usd
|
|
485
|
+
FROM selected f LEFT JOIN attempts a ON a.root_ref=f.root_ref ORDER BY f.sort_at DESC,f.root_ref DESC`)
|
|
486
|
+
.all({ ...parameters(now, access, { project }, custodyReadings(store)), $since: since, $outcomeLimit: limit + 1 });
|
|
487
|
+
}
|
|
456
488
|
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
457
489
|
* list's own rows, so a completed family (root or any revision), a failed
|
|
458
490
|
* check and a superseded version never count. */
|