fix(dot): synchronize human assignment and preserve Unicode pages

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-07 19:00:52 -05:00
1 parent 155c825315
commit eefd3f22ea
5 files changed
+58 -5

No files matched your search

@@ -2149,7 +2149,15 @@
"additionalProperties": false,
"properties": {
"assigneeActorId": {
"description": "Optional actor assignee.",
"description": "Optional actor assignee. Mutually exclusive with assigneeUserId.",
"maxLength": 200,
"type": [
"string",
"null"
]
},
"assigneeUserId": {
"description": "Company person ID from list_people. Mutually exclusive with assigneeActorId.",
"maxLength": 200,
"type": [
"string",
@@ -49,6 +49,18 @@ describe("semantic action catalog", () => {
}
});
it("advertises human assignment in both task creation contracts", () => {
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: true });
for (const schema of [createTaskAction.live.descriptor.inputSchema, paperclipSemanticAction("create_task")!.inputSchema]) {
const validate = ajv.compile(schema);
const input = { title: "Hello", idempotencyKey: "hello", assigneeUserId: "company-owner" };
expect(validate(input), JSON.stringify(validate.errors)).toBe(true);
expect(validate({ ...input, assigneeUserId: null })).toBe(true);
expect(validate({ ...input, assigneeUserId: 42 })).toBe(false);
expect(validate({ ...input, undeclaredField: true })).toBe(false);
}
});
it("accepts project handoff receipts and preserves ordinary child task receipts", () => {
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: true });
const validate = ajv.compile(createTaskAction.live.descriptor.outputSchema);
@@ -520,7 +520,8 @@ const descriptors: readonly PaperclipSemanticActionDescriptor[] = [
projectId: nullableText("Project identifier for the new task."),
initialPlan: nullableText("Remaining execution steps to persist as the task plan. Exclude completed planning, approval, and handoff steps; cite the source plan revision and approval. A copied plan is not a new approval gate."),
description: nullableText("Child task description."),
assigneeActorId: nullableText("Optional actor assignee.", 200),
assigneeActorId: nullableText("Optional actor assignee. Mutually exclusive with assigneeUserId.", 200),
assigneeUserId: nullableText("Company person ID from list_people. Mutually exclusive with assigneeActorId.", 200),
status: { enum: ["backlog", "todo"], description: "Initial status. Use backlog to save work without execution. Defaults to todo (blocked when dependencies are unresolved)." },
priority: { enum: ["critical", "high", "medium", "low"] },
blockedByTaskIds: stringArray("Initial blocker task identifiers."),
@@ -26,6 +26,31 @@ describe("Runner workspace bridge", () => {
expect((await readWorkspaceUploadFile(directory, "hello.txt")).toString()).toBe("updated");
await expect(executeWorkspaceTool(directory, "workspace_read", { path: "outside/private.txt" }, authorize)).rejects.toThrow("symlink");
});
it.each(["workspace", "skill"])("preserves Unicode characters across %s text pages", async mode => {
const text = "a".repeat(5999) + "🌍" + "z".repeat(6000) + "🚀";
const directory = await root();
await writeFile(join(directory, "unicode.txt"), text);
const bundle = await materializeAsset([{ path: "SKILL.md", content: Buffer.from(text), mode: 0o444 }]);
const context = { skills: [{ key: "unicode", runtimeName: "unicode", versionId: "version-1", bundle }] } as any;
const read = async (offset: number) => (mode === "workspace"
? executeWorkspaceTool(directory, "workspace_read", { path: "unicode.txt", offset }, authorize)
: readAssignedSkill(context, { skill: "unicode", offset })) as Promise<{ text: string; nextOffset: number | null; sha256: string; byteSize: number }>;
const first = await read(0);
expect(first.nextOffset).toBe(5999);
expect(first.text).toBe("a".repeat(5999));
let combined = first.text, next = first.nextOffset;
while (next !== null) {
const page = await read(next);
expect(page.sha256).toBe(first.sha256);
expect(page.byteSize).toBe(Buffer.byteLength(text));
expect(page.text).not.toMatch(/^[\uDC00-\uDFFF]|[\uD800-\uDBFF]$/u);
combined += page.text;
next = page.nextOffset;
}
expect(combined).toBe(text);
await expect(read(6000)).rejects.toThrow("offset_splits_character");
});
it("serializes overlapping writes so only one observed hash can commit", async () => {
const directory = await root();
const initial = await executeWorkspaceTool(directory, "workspace_write", { path: "shared.txt", text: "initial", expectedSha256: null }, authorize) as any;
@@ -67,10 +67,17 @@ async function readFilePage(file: string, offset: number) {
const stat = await handle.stat();
if (!stat.isFile() || stat.size > 16 * 1024 * 1024) throw new Error("runner_bridge_file_size_limit");
const bytes = await handle.readFile();
// Character pagination avoids splitting UTF-8 sequences; size and hash remain byte-based.
// Offsets remain UTF-16 units, but page boundaries preserve complete code points.
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const page = text.slice(offset, offset + 6000);
return { text: page, sha256: digest(bytes), byteSize: bytes.length, nextOffset: offset + page.length < text.length ? offset + page.length : null };
const splitsCharacter = (at: number) => {
const before = text.charCodeAt(at - 1), after = text.charCodeAt(at);
return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
};
if (splitsCharacter(offset)) throw new Error("runner_bridge_offset_splits_character");
let end = Math.min(offset + 6000, text.length);
if (splitsCharacter(end)) end--;
const page = text.slice(offset, end);
return { text: page, sha256: digest(bytes), byteSize: bytes.length, nextOffset: end < text.length ? end : null };
} finally { await handle.close(); }
}
export async function readAssignedSkill(context: NativeRuntimeContextSnapshot, raw: unknown) {