mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-09 16:35:27 +02:00
Mark missing and invalid child run ordinals as partial observations. Exercise the pinned vendor child construction and reuse methods on all three platforms and regenerate the Cursor-only execution identity. Co-Authored-By: Paperclip <noreply@paperclip.ing>
82 lines
4.3 KiB
JavaScript
82 lines
4.3 KiB
JavaScript
/** Self-contained: embedded verbatim into the digest-pinned ACP module.
|
|
* Observations are NOT usage accounting. Native counter semantics remain unknown.
|
|
*/
|
|
export function createCursorNativeUsage(promptId) {
|
|
if (typeof promptId !== "string" || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(promptId)) throw new Error("Invalid Cursor usage prompt identity");
|
|
const limits = { maxObservations: 64, maxInvocations: 64, maxBytes: 16384 };
|
|
// Reserve bounded ACPX request/message identities and wrapper keys within 16 KiB.
|
|
const envelopeMaxBytes = limits.maxBytes - 640;
|
|
const fields = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"];
|
|
const reasons = new Set(["native_counter_semantics_unverified"]);
|
|
const observations = [];
|
|
const children = new WeakMap();
|
|
const trackedInvocations = new Set(); // At most maxInvocations, including children.
|
|
let closed = false, truncated = false, invocations = 0;
|
|
let finished;
|
|
const envelope = () => ({
|
|
schema: "paperclip.cursor.native-usage.v1", source: "native_turn_ended", promptId,
|
|
completeness: "partial", reasons: [...reasons], observations, limits, truncated,
|
|
});
|
|
const truncate = () => { truncated = true; reasons.add("observation_limit_reached"); };
|
|
function begin(role, nativeRun) {
|
|
if (closed) return null;
|
|
if (invocations >= limits.maxInvocations) { truncate(); return null; }
|
|
const invocationId = `invocation-${++invocations}`;
|
|
let ended = false, sequence = 0;
|
|
const collector = {
|
|
observe(update) {
|
|
if (closed || ended || truncated || update?.message?.case !== "turnEnded") return;
|
|
if (observations.length >= limits.maxObservations) { truncate(); return; }
|
|
const value = update.message.value;
|
|
const counters = {};
|
|
for (const field of fields) {
|
|
const raw = value?.[field];
|
|
if (raw === undefined) continue;
|
|
const number = typeof raw === "bigint" && raw >= 0n && raw <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(raw) : raw;
|
|
if (typeof number === "number" && Number.isSafeInteger(number) && number >= 0) counters[field] = number;
|
|
else reasons.add("invalid_native_counter");
|
|
}
|
|
if (Object.keys(counters).length < fields.length) reasons.add("native_counters_missing");
|
|
if (sequence > 0) reasons.add("multiple_terminal_observations");
|
|
observations.push({ invocationId, role, nativeRun, sequence: ++sequence, counters });
|
|
},
|
|
end() { if (!ended && sequence === 0 && !closed) reasons.add("native_terminal_not_observed"); ended = true; },
|
|
};
|
|
trackedInvocations.add(collector);
|
|
return collector;
|
|
}
|
|
return {
|
|
beginParent() { return begin("parent", invocations + 1); },
|
|
observeChild(child, update) {
|
|
if (closed || !child || child.terminal) return;
|
|
if (!Number.isSafeInteger(child.runs) || child.runs < 1) { reasons.add("child_run_attribution_unverified"); return; }
|
|
// The native callback identifies only the child, not its run generation.
|
|
// After ID reuse, delayed old-run updates cannot be distinguished safely.
|
|
if (child.runs !== 1) { reasons.add("child_run_attribution_unverified"); return; }
|
|
let run = children.get(child);
|
|
if (!run || run.nativeRun !== child.runs) {
|
|
run?.collector?.end();
|
|
run = { nativeRun: child.runs, collector: begin("child", child.runs) };
|
|
children.set(child, run);
|
|
}
|
|
run.collector?.observe(update);
|
|
},
|
|
finish() {
|
|
if (finished !== undefined) return JSON.parse(finished);
|
|
for (const invocation of trackedInvocations) invocation.end();
|
|
trackedInvocations.clear();
|
|
closed = true;
|
|
if (observations.length === 0) reasons.add("native_terminal_not_observed");
|
|
// Bound the FINAL envelope after every reason has been added. Its fixed
|
|
// ASCII keys/IDs/reasons make JSON length equal to its UTF-8 byte length.
|
|
finished = JSON.stringify(envelope());
|
|
while (finished.length > envelopeMaxBytes && observations.length > 0) {
|
|
observations.pop(); truncate(); finished = JSON.stringify(envelope());
|
|
}
|
|
// Detached snapshot: late provider callbacks cannot mutate a returned receipt.
|
|
return JSON.parse(finished);
|
|
},
|
|
close() { closed = true; trackedInvocations.clear(); },
|
|
};
|
|
}
|