Fix completed-history owner receipts and verified attachment cleanup

Use truthful storage absence receipts for MCP deletion and preserve replay fences. Qualify retained chat controls and provider submissions through their actual owners, retain voice and registration obligations, and bound delivery receipt lookup.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-02 14:12:41 -06:00
1 parent aaf55692c4
commit 56acdfdffa
31 files changed
+1849 -98

No files matched your search

+24
View File
@@ -118,6 +118,30 @@ Failed company transfers remain resumable. Completed transfers require their
recorded manifest, exact part receipts and matching counts. Completed/cancelled
transfers still block while their registered local spool contains cleanup work.
Chat control tokens can remain issued or become expired after another actor
resolves their canonical interaction. Their exact interaction, publication and
conversation receipts establish completion; a token status alone does not.
Provider submission receipts do not exempt pending source wakes or publications.
Photon attachment provenance remains immutable history after ordinary attachment
removal, but any surviving metadata must agree with its recorded identities and
hashes. This proof establishes completed registration, not storage cleanup.
Public-MCP upload cleanup requires both the existing cleanup stamp and a new
verified-cleanup stamp. Ordinary attachment and issue removal write this proof
only after successful object deletion and an absence readback. Legacy cleanup
stamps are not backfilled: the existing bounded opportunistic sweep can verify
expired or orphaned uploads. Reissuing an uncommitted upload clears both stamps;
a committed/deleted request replay preserves them. An expired or cleaned upload
cannot accept a still-valid old transport ticket.
Voice report refresh remains work for the owner's full 24-hour window after a
provider session ends, even when the report is available. Live session recovery,
unconfirmed inbound cleanup and ambiguous reply submission still block. A
completed provider submission does not prove spoken playback. Speko setup also
requires the existing successful whole-configuration receipt after its individual
tool and webhook writes. Discord command registration retains its own future
check; a completed command invocation does not retire that deadline.
Waiting agent conversations can be passive containers under both ordinary and
scheduled inspection. The same narrow proof requires the exact conversation
agent/company/user, waiting state, no checkout, active run, accounting, monitor
@@ -0,0 +1 @@
ALTER TABLE "mcp_attachment_uploads" ADD COLUMN IF NOT EXISTS "cleanup_verified_at" timestamp with time zone;
@@ -1,6 +1,6 @@
{
"id": "c777fa84-85a6-418f-bea1-0d60b56cf071",
"prevId": "c2c69a4a-8281-419c-9eeb-a7bcbc2beaae",
"id": "aef8cf7c-1939-4841-bb4a-b20203f10b69",
"prevId": "fe0b877c-d63e-46d9-8798-c7729fd13eb7",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -2233,6 +2233,74 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_file_roots": {
"name": "agent_file_roots",
"schema": "",
"columns": {
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"generation": {
"name": "generation",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"entries": {
"name": "entries",
"type": "jsonb",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"agent_file_roots_company_id_agent_id_agents_company_id_id_fk": {
"name": "agent_file_roots_company_id_agent_id_agents_company_id_id_fk",
"tableFrom": "agent_file_roots",
"tableTo": "agents",
"columnsFrom": [
"company_id",
"agent_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"agent_file_roots_company_id_agent_id_pk": {
"name": "agent_file_roots_company_id_agent_id_pk",
"columns": [
"company_id",
"agent_id"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_identity_keys": {
"name": "agent_identity_keys",
"schema": "",
@@ -29705,6 +29773,241 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.execution_workspace_snapshots": {
"name": "execution_workspace_snapshots",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"execution_workspace_id": {
"name": "execution_workspace_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"source_operation_id": {
"name": "source_operation_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"source_run_id": {
"name": "source_run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"parent_snapshot_id": {
"name": "parent_snapshot_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"generation": {
"name": "generation",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"epoch": {
"name": "epoch",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"format": {
"name": "format",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'paperclip.remote-workspace-archive.v1'"
},
"object_key": {
"name": "object_key",
"type": "text",
"primaryKey": false,
"notNull": false
},
"sha256": {
"name": "sha256",
"type": "text",
"primaryKey": false,
"notNull": false
},
"byte_size": {
"name": "byte_size",
"type": "bigint",
"primaryKey": false,
"notNull": false
},
"key": {
"name": "key",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"manifest_digest": {
"name": "manifest_digest",
"type": "text",
"primaryKey": false,
"notNull": false
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"delete_after": {
"name": "delete_after",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"last_error": {
"name": "last_error",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"execution_workspace_snapshots_operation_uniq": {
"name": "execution_workspace_snapshots_operation_uniq",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "source_operation_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"execution_workspace_snapshots_generation_uniq": {
"name": "execution_workspace_snapshots_generation_uniq",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "generation",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"execution_workspace_snapshots_workspace_idx": {
"name": "execution_workspace_snapshots_workspace_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "execution_workspace_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"execution_workspace_snapshots_cleanup_idx": {
"name": "execution_workspace_snapshots_cleanup_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "delete_after",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.execution_workspaces": {
"name": "execution_workspaces",
"schema": "",
@@ -37105,6 +37408,15 @@
"client_request_id"
]
},
"issue_comments_agent_client_request_uq": {
"name": "issue_comments_agent_client_request_uq",
"nullsNotDistinct": false,
"columns": [
"issue_id",
"author_agent_id",
"client_request_id"
]
},
"issue_comments_company_id_uq": {
"name": "issue_comments_company_id_uq",
"nullsNotDistinct": false,
@@ -52770,6 +53082,12 @@
"primaryKey": false,
"notNull": false
},
"next_eval_reason": {
"name": "next_eval_reason",
"type": "text",
"primaryKey": false,
"notNull": false
},
"next_eval_at": {
"name": "next_eval_at",
"type": "timestamp with time zone",
@@ -62124,6 +62442,12 @@
"primaryKey": false,
"notNull": false
},
"cleanup_verified_at": {
"name": "cleanup_verified_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
+7
View File
@@ -2948,6 +2948,13 @@
"when": 1793503729899,
"tag": "0422_eminent_darwin",
"breakpoints": true
},
{
"idx": 423,
"version": "7",
"when": 1793645545010,
"tag": "0423_glossy_mariko_yashida",
"breakpoints": true
}
]
}
+3
View File
@@ -188,6 +188,9 @@ export const mcpAttachmentUploads = pgTable("mcp_attachment_uploads", {
storageProvider: text("storage_provider").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
cleanedAt: timestamp("cleaned_at", { withTimezone: true }),
// Written only after storage deletion and an authoritative absence readback.
// Legacy cleanedAt can predate truthful local filesystem error handling.
cleanupVerifiedAt: timestamp("cleanup_verified_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (t) => [
uniqueIndex("mcp_attachment_upload_request_uq").on(t.companyId, t.userId, t.requestId),
@@ -1,3 +1,5 @@
import { chatHistoryActionBlocks } from "./helpers/chat-history-proof.js";
import { resolveExternalTestDatabaseUrl } from "./helpers/external-test-database.js";
import { resolveCommittedNativeRecoveryNotices, pendingNativeRecoveryNotices } from "../services/native-runtime/native-recovery-notices.js";
import { issueRecoveryActionService } from "../services/issue-recovery-actions.js";
import { projectCommittedRun } from "../services/native-runtime/native-run-projection.js";
@@ -284,7 +286,8 @@ import {
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const externalTestDatabaseUrl = process.env.PAPERCLIP_TEST_DATABASE_URL;
const historyTestDatabaseUrl = resolveExternalTestDatabaseUrl("PAPERCLIP_HISTORY_TEST_DATABASE_URL");
const externalTestDatabaseUrl = historyTestDatabaseUrl ?? process.env.PAPERCLIP_TEST_DATABASE_URL;
const embeddedPostgresSupport = externalTestDatabaseUrl
? { supported: true }
: await getEmbeddedPostgresTestSupport();
@@ -1122,6 +1125,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
// Do not expire or time out its connections on those virtual clock jumps.
db = createDb(externalTestDatabaseUrl ?? tempDb!.connectionString, {
idleTimeoutSeconds: 0, connectTimeoutSeconds: 0, maxLifetimeSeconds: 0,
...(historyTestDatabaseUrl ? { maxConnections: 4 } : {}),
});
}, 90_000);
@@ -3006,6 +3010,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
const [review] = await readChatGitHubReviewsByEndpointId(f.endpoint.id);
expect(review.assessment?.findings[0].category).toBe(categories[0]);
expect((await f.action()).status).toBe("processed");
expect(await chatHistoryActionBlocks(db, (await f.action()).id)).toBe(false);
expect(f.responseComments.size).toBe(1);
});
it("publishes one score heading when a newly configured bot submits a formatted summary", async () => {
@@ -3069,6 +3074,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
const [response] = await db.select().from(chatActions).where(and(eq(chatActions.deliveryId, delivery.id),
eq(chatActions.kind, "github_response_comment")));
expect(response).toMatchObject({ status: "cancelled", result: { final: true, code: "passive_response_wait" } });
expect(await chatHistoryActionBlocks(db, response.id)).toBe(false);
const writes = f.responseWrites.length;
const updatedAt = response.updatedAt;
await responses.processPending();
@@ -3314,6 +3320,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
const body = [...f.responseComments.values()][0].body;
expect(body).not.toMatch(/Review queued|Working on this/);
expect(body).toContain("paperclip-response-state:final");
const responses = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "github_response_comment")));
expect(responses.length).toBeGreaterThan(0);
for (const receipt of responses) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(false);
});
it("retires a legacy coalesced GitHub placeholder before replacing its receipt", async () => {
const f = await publicationRetryFixture();
@@ -3425,6 +3434,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
expect([...f.responseComments.values()].some(comment => comment.body.includes("Newer progress"))).toBe(true);
const [settled] = await db.select().from(chatActions).where(eq(chatActions.id, first.id));
expect(settled.result?.final).toBe(false);
expect(await chatHistoryActionBlocks(db, settled.id)).toBe(true);
});
it.each([false, true])("recovers an approved queued GitHub review without another approval (revoked: %s)", async revoked => {
const f = await publicationRetryFixture();
@@ -3460,6 +3470,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
await db.update(chatActions).set({ result: { ...action.result, retryAt: new Date(0).toISOString() } }).where(eq(chatActions.id, action.id));
await f.service.processPending();
expect((await f.action()).status).toBe(revoked ? "cancelled" : "processed");
expect(await chatHistoryActionBlocks(db, (await f.action()).id)).toBe(false);
if (revoked) await expect(gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: f.assessment })).rejects.toThrow();
else expect(await gateway.executeTool({ sessionToken: session.token, tool: tool.name, parameters: f.assessment }))
.toMatchObject({ status: "replayed", result: { data: { status: "processed", score: 3 } } });
@@ -4830,6 +4841,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
expect(f.responseWrites.filter(write => write.method === "POST")).toHaveLength(2);
const [deleted] = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "github_response_comment"), sql`${chatActions.result}->>'id' = ${String(firstId)}`));
expect(deleted).toMatchObject({ status: "cancelled", result: { final: true, code: "response_deleted" } });
expect(await chatHistoryActionBlocks(db, deleted.id)).toBe(false);
await githubResponseCommentService(db, f.providerFetch).processPending();
expect(f.responseComments.size).toBe(1);
await expect(githubRunReplyState(db, { ...f.session, endpointId: f.endpoint.id })).resolves.toBe("confirmed");
@@ -9767,6 +9779,9 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.manifest.create"))).toHaveLength(0);
expect((await f.service.get(f.endpoint.id)).status).not.toBe("revoked");
expect((await f.service.get(f.endpoint.id)).setup.slackRegistration).toMatchObject({ status: "configured", appId: f.appId });
const [reconnectReceipt] = await db.select().from(chatActions).where(and(eq(chatActions.endpointId, f.endpoint.id), eq(chatActions.kind, "slack_managed_reconnect")));
expect(reconnectReceipt).toMatchObject({ status: "processed", result: null });
expect(await chatHistoryActionBlocks(db, reconnectReceipt.id)).toBe(false);
await f.service.slackRegistration.managed.provision(f.endpoint.id, actor, reconnectInput);
await f.provision();
expect(f.provider.mock.calls.filter(([url]) => String(url).endsWith("apps.managedInstall"))).toHaveLength(1);
@@ -17019,12 +17034,13 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
context.endpoint.id,
);
expect(context.posts).toEqual([]);
if (change === "edited comment")
if (change === "edited comment") {
expect(await context.receipt()).toMatchObject({
status: "cancelled",
result: { attempts: 0, code: "source_changed_or_unavailable" },
});
else expect(await context.receipt()).toBeUndefined();
expect(await chatHistoryActionBlocks(db, (await context.receipt())!.id)).toBe(false);
} else expect(await context.receipt()).toBeUndefined();
expect(context.wakeup).not.toHaveBeenCalled();
} finally {
await retirePublicationFixture(context.service, context.endpoint.id);
@@ -17705,6 +17721,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
result: { code: "github_webhook_ingress_runtime_superseded" },
});
expect((await context.ingress())!.payload).not.toHaveProperty("body");
expect(await chatHistoryActionBlocks(db, (await context.ingress())!.id)).toBe(false);
expect(sdkDispatch).not.toHaveBeenCalled();
expect(
await db
@@ -17756,6 +17773,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
result: { code: "github_webhook_recovery_source_changed" },
});
expect(cancelled!.payload).not.toHaveProperty("body");
expect(await chatHistoryActionBlocks(db, cancelled!.id)).toBe(false);
expect(cancelled!.result?.retryable).not.toBe(true);
expect(context.requests).toEqual([]);
expect(
@@ -43530,6 +43548,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
expect(original?.state).toBe("published");
expect(original?.payload.card?.actions).toEqual(expect.arrayContaining([expect.objectContaining({ type: "callback" })]));
if (kind === "connection_intent") expect(original?.payload.card?.actions).toContainEqual({ type: "link", label: "Review connection", url: expect.any(String) });
const controls = (await db.select().from(chatActions).where(and(eq(chatActions.endpointId, endpoint.id),
eq(sql<string>`${chatActions.payload}->>'interactionId'`, interaction.id))))
.filter(row => ["confirmation_response", "question_form_open", "question_form_submit", "question_answer"].includes(row.kind));
expect(controls.length).toBeGreaterThan(0);
for (const control of controls) expect(await chatHistoryActionBlocks(db, control.id)).toBe(true);
if (kind === "connection_intent") await interactions.resolveConnectionIntent(issue, interaction.id, { version: 1, outcome: "declined" }, { userId: "owner-user" });
else if (kind === "request_item_verdicts") await interactions.submitItemVerdicts(issue, interaction.id, { verdicts: [{ id: "docs", verdict: "approve" }] }, { userId: "owner-user" });
else await interactions.rejectInteraction(issue, interaction.id, {}, { userId: "owner-user" });
@@ -43539,6 +43562,11 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
expect(terminal?.state).toBe("published");
expect(terminal?.providerMessageId).toBe(original.providerMessageId);
expect(terminal?.payload.card?.actions).toBeUndefined();
for (const control of controls) expect(await chatHistoryActionBlocks(db, control.id)).toBe(false);
const control = controls[0]!;
await db.update(chatActions).set({ payload: { ...control.payload, interactionId: randomUUID() } }).where(eq(chatActions.id, control.id));
expect(await chatHistoryActionBlocks(db, control.id)).toBe(true);
await db.update(chatActions).set({ payload: control.payload }).where(eq(chatActions.id, control.id));
} finally {
await service.shutdown();
if (previousPublicUrl === undefined) delete process.env.PAPERCLIP_PUBLIC_URL;
@@ -80927,6 +80955,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
expect(context.consentCards).toHaveLength(1);
expect(context.fileCards).toEqual([]);
expect(context.uploadRequest).not.toHaveBeenCalled();
expect(await chatHistoryActionBlocks(db, intent!.id)).toBe(true);
const before = await context.service.getPublicationBatchStatus(
context.endpoint.id,
context.conversation.id,
@@ -80955,6 +80984,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
body: context.bytes,
});
expect(context.fileCards).toHaveLength(1);
expect(await chatHistoryActionBlocks(db, intent!.id)).toBe(false);
const status = await context.service.getPublicationBatchStatus(
context.endpoint.id,
context.conversation.id,
@@ -0,0 +1,11 @@
import { sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { idleChatHistoryChecks } from "../../services/chat-channels/idle-history-proof.js";
/** Scope the actual shared predicate to a real producer's receipt. Other pending
* fixture work remains independently observable through its owning gates. */
export async function chatHistoryActionBlocks(db: Db, actionId: string): Promise<boolean> {
const check = idleChatHistoryChecks.find(check => check.reason === "chat_actions")!;
const [row] = await db.execute<{ blocked: boolean }>(sql`SELECT EXISTS (${sql.raw(check.query)} AND a.id = ${actionId}::uuid) AS blocked`);
return row.blocked;
}
@@ -1,5 +1,5 @@
import { createHash, randomBytes, randomUUID } from "node:crypto";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { createHash, generateKeyPairSync, randomBytes, randomUUID } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import net from "node:net";
@@ -33,6 +33,11 @@ const support = externalDatabaseUrl ? { supported: true } : await getEmbeddedPos
if (elapsed > 1000) console.info(`History predicate ${reason}: ${Math.round(elapsed)}ms`);
return row.blocked;
};
const blockedActionKind = async (kind: string) => {
const query = completedHistoryChecks.find(check => check.reason === "chat_actions")!.query;
const [row] = await db.execute<{ blocked: boolean }>(sql`SELECT EXISTS (${sql.raw(query)} AND a.kind = ${kind}) AS blocked`);
return row.blocked;
};
beforeAll(async () => {
if (!externalDatabaseUrl) database = await startEmbeddedPostgresTestDatabase("idle-history-");
db = tables.createDb(externalDatabaseUrl ?? database!.connectionString, { maxConnections: 4 });
@@ -107,10 +112,28 @@ const support = externalDatabaseUrl ? { supported: true } : await getEmbeddedPos
await db.insert(tables.mcpOauthTokens).values({ grantId: p.grantId, kind: "access", tokenHash: hashMcpSecret(token),
expiresAt: new Date(Date.now() + 600_000) });
const directory = await mkdtemp(path.join(tmpdir(), "history-mcp-storage-"));
const router = Router();
router.use(issueRoutes(db, createStorageService(createLocalDiskStorageProvider(directory))));
const router = Router(), storage = createStorageService(createLocalDiskStorageProvider(directory));
router.use(issueRoutes(db, storage));
const oauth = createPublicMcpOAuth(db, { origin, resource }), api = createMcpApiDispatch(router);
return { ...p, token, oauth, api, storage: createStorageService(createLocalDiskStorageProvider(directory)), cleanup: () => rm(directory, { recursive: true, force: true }) };
return { ...p, token, oauth, api, storage, cleanup: () => rm(directory, { recursive: true, force: true }) };
}
async function slackFixture() {
const f = await seed(), c = await connection(f), p = await mcp(f);
await db.update(tables.toolConnections).set({ enabled: true, status: "active" }).where(eq(tables.toolConnections.id, c.connectionId));
await db.insert(tables.companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: p.userId,
status: "active", membershipRole: "owner" });
const [endpoint] = await db.insert(tables.chatEndpoints).values({ companyId: f.companyId, connectionId: c.connectionId,
assignedAgentId: f.agentId, publicId: randomUUID(), provider: "slack", status: "active", providerAccountId: "TWORKSPACE",
botExternalId: "UBOT", allowDirectMessages: true, publicationMode: "automatic" }).returning();
const [conversation] = await db.insert(tables.chatConversations).values({ companyId: f.companyId, endpointId: endpoint.id,
issueId: f.issueId, externalConversationId: "DCHANNEL", externalThreadId: "slack:DCHANNEL:123.456", externalLabel: "Personal",
state: "active", isDirectMessage: true }).returning();
const [principal] = await db.insert(tables.chatExternalPrincipals).values({ companyId: f.companyId, provider: "slack",
providerAccountId: "TWORKSPACE", externalId: "UREQUESTER", kind: "user", isBot: false }).returning();
const authority = { endpoint, issueId: f.issueId, conversation, userId: p.userId, slackUserId: "UREQUESTER", principalId: principal.id,
deliveryId: null, identityContextId: null, revision: "fixture-revision", workMode: "standard", botToken: "fixture-bot-token" };
return { ...f, ...p, ...c, endpoint, conversation, principal, authority };
}
it("checks all 32 families on the real migrated schema and keeps an empty instance eligible", async () => {
@@ -166,13 +189,117 @@ const support = externalDatabaseUrl ? { supported: true } : await getEmbeddedPos
const result = await request(app).put(url.pathname + url.search).set("Content-Type", "text/plain").send(bytes).timeout({ response: 5000, deadline: 10_000 });
expect(result.status).toBe(200);
expect(await blocked("mcp_attachment_uploads")).toBe(false);
const abandoned = await transfers.getUploadUrl(principal, { ...args, requestId: randomUUID() });
await db.update(tables.mcpAttachmentUploads).set({ cleanedAt: now, cleanupVerifiedAt: now }).where(eq(tables.mcpAttachmentUploads.id, String(upload.uploadId)));
expect(await blocked("mcp_attachment_uploads")).toBe(true);
await db.update(tables.mcpAttachmentUploads).set({ cleanedAt: null, cleanupVerifiedAt: null }).where(eq(tables.mcpAttachmentUploads.id, String(upload.uploadId)));
const abandonedArgs = { ...args, requestId: randomUUID() };
const abandoned = await transfers.getUploadUrl(principal, abandonedArgs);
expect(await blocked("mcp_attachment_uploads")).toBe(true);
const [old] = await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, String(abandoned.uploadId)));
await p.storage.putFile({ companyId: f.companyId, namespace: "mcp-transfers", objectKey: old.objectKey,
originalFilename: args.filename, contentType: args.contentType, body: bytes });
// Model the old owner's optimistic cleanup: its stamp exists while bytes
// survive. It must stay blocked until actual deletion and readback.
await db.update(tables.mcpAttachmentUploads).set({ expiresAt: new Date(0), cleanedAt: new Date() }).where(eq(tables.mcpAttachmentUploads.id, old.id));
expect(await p.storage.headObject(f.companyId, old.objectKey)).toMatchObject({ exists: true });
expect(await blocked("mcp_attachment_uploads")).toBe(true);
await db.update(tables.mcpAttachmentUploads).set({ expiresAt: new Date(0) }).where(eq(tables.mcpAttachmentUploads.id, String(abandoned.uploadId)));
await transfers.getDownloadUrl(principal, result.body.attachment.id);
expect(await p.storage.headObject(f.companyId, old.objectKey)).toEqual({ exists: false });
expect(await blocked("mcp_attachment_uploads")).toBe(false);
await db.update(tables.mcpAttachmentUploads).set({ attachmentId: null }).where(eq(tables.mcpAttachmentUploads.id, String(upload.uploadId)));
const verified = (await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, old.id)))[0];
const staleUrl = new URL(String(abandoned.url));
expect((await request(app).put(staleUrl.pathname + staleUrl.search).set("Content-Type", "text/plain").send(bytes)
.timeout({ response: 5000, deadline: 10_000 })).status).toBe(403);
expect(await p.storage.headObject(f.companyId, old.objectKey)).toEqual({ exists: false });
expect((await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, old.id)))[0]).toEqual(verified);
const reissued = await transfers.getUploadUrl(principal, abandonedArgs);
expect((await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, old.id)))[0])
.toMatchObject({ cleanedAt: null, cleanupVerifiedAt: null });
expect(await blocked("mcp_attachment_uploads")).toBe(true);
const secondUrl = new URL(String(reissued.url));
const second = await request(app).put(secondUrl.pathname + secondUrl.search).set("Content-Type", "text/plain").send(bytes)
.timeout({ response: 5000, deadline: 10_000 });
expect(second.status).toBe(200);
for (const attachment of [result.body.attachment, second.body.attachment]) {
expect(await p.api(principal, "DELETE", `/attachments/${attachment.id}`)).toEqual({ ok: true });
}
const [removed] = await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, String(upload.uploadId)));
expect(removed).toMatchObject({ attachmentId: result.body.attachment.id, cleanedAt: expect.any(Date), cleanupVerifiedAt: expect.any(Date) });
expect(await p.storage.headObject(f.companyId, removed.objectKey)).toEqual({ exists: false });
expect(await blocked("mcp_attachment_uploads")).toBe(false);
await expect(transfers.getUploadUrl(principal, args)).rejects.toThrow();
expect((await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, removed.id)))[0]).toEqual(removed);
expect(await blocked("mcp_attachment_uploads")).toBe(false);
await db.update(tables.mcpAttachmentUploads).set({ cleanupVerifiedAt: null }).where(eq(tables.mcpAttachmentUploads.id, removed.id));
expect(await blocked("mcp_attachment_uploads")).toBe(true);
} finally { await p.cleanup(); }
}, 60_000);
it.each(["delete_failed", "object_survived", "head_failed"] as const)("retains MCP cleanup uncertainty after actual attachment removal: %s", async failure => {
const { createPublicMcpTransfers } = await import("../services/public-mcp/file-transfers.js");
const { default: express } = await import("express");
const { default: request } = await import("supertest");
const f = await seed(), p = await connectedMcp(f);
try {
const principal = await p.oauth.authenticate(p.token), bytes = Buffer.from("retained uncertain attachment");
const transfers = createPublicMcpTransfers(db, p.oauth, p.api, p.storage);
const upload = await transfers.getUploadUrl(principal, { companyId: f.companyId, taskId: f.issueId, requestId: randomUUID(),
filename: "uncertain.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") });
const app = express(); app.use(transfers.router);
const url = new URL(String(upload.url));
const result = await request(app).put(url.pathname + url.search).set("Content-Type", "text/plain").send(bytes)
.timeout({ response: 5000, deadline: 10_000 });
expect(result.status).toBe(200);
const [before] = await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, String(upload.uploadId)));
const originalDelete = p.storage.deleteObject, originalHead = p.storage.headObject;
if (failure === "delete_failed") p.storage.deleteObject = async () => { throw new Error("controlled storage removal failure"); };
if (failure === "object_survived") p.storage.deleteObject = async () => {};
if (failure === "head_failed") p.storage.headObject = async () => { throw new Error("controlled storage readback failure"); };
try { expect(await p.api(principal, "DELETE", `/attachments/${result.body.attachment.id}`)).toEqual({ ok: true }); }
finally { p.storage.deleteObject = originalDelete; p.storage.headObject = originalHead; }
expect(await db.select().from(tables.issueAttachments).where(eq(tables.issueAttachments.id, result.body.attachment.id))).toHaveLength(0);
expect((await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, before.id)))[0])
.toMatchObject({ attachmentId: before.attachmentId, cleanedAt: null, cleanupVerifiedAt: null });
expect((await p.storage.headObject(f.companyId, before.objectKey)).exists).toBe(failure !== "head_failed");
expect(await blocked("mcp_attachment_uploads")).toBe(true);
// The existing bounded opportunistic owner can later retire this orphan
// only by a real successful delete and readback, preserving its identity.
await db.update(tables.mcpAttachmentUploads).set({ expiresAt: new Date(0) }).where(eq(tables.mcpAttachmentUploads.id, before.id));
const pending = await transfers.getUploadUrl(principal, { companyId: f.companyId, taskId: f.issueId, requestId: randomUUID(),
filename: "new.txt", contentType: "text/plain", byteSize: 1, sha256: createHash("sha256").update("x").digest("hex") });
expect((await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, before.id)))[0])
.toMatchObject({ attachmentId: before.attachmentId, cleanupVerifiedAt: expect.any(Date) });
expect(await p.storage.headObject(f.companyId, before.objectKey)).toEqual({ exists: false });
expect(await blocked("mcp_attachment_uploads")).toBe(true); // newly allocated upload remains pending
expect(pending.uploadId).toBeTruthy();
} finally { await p.cleanup(); }
}, 60_000);
it.each([false, true])("preserves verified MCP cleanup after ordinary issue deletion, storageFailure=%s", async storageFailure => {
const { createPublicMcpTransfers } = await import("../services/public-mcp/file-transfers.js");
const { default: express } = await import("express");
const { default: request } = await import("supertest");
const f = await seed(), p = await connectedMcp(f);
try {
const principal = await p.oauth.authenticate(p.token), bytes = Buffer.from("removed with parent");
const transfers = createPublicMcpTransfers(db, p.oauth, p.api, p.storage);
const upload = await transfers.getUploadUrl(principal, { companyId: f.companyId, taskId: f.issueId, requestId: randomUUID(),
filename: "parent.txt", contentType: "text/plain", byteSize: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") });
const app = express(); app.use(transfers.router);
const url = new URL(String(upload.url));
const result = await request(app).put(url.pathname + url.search).set("Content-Type", "text/plain").send(bytes)
.timeout({ response: 5000, deadline: 10_000 });
expect(result.status).toBe(200);
const originalDelete = p.storage.deleteObject;
if (storageFailure) p.storage.deleteObject = async () => { throw new Error("controlled parent storage failure"); };
try { await p.api(principal, "DELETE", `/issues/${f.issueId}`); }
finally { p.storage.deleteObject = originalDelete; }
expect(await db.select().from(tables.issues).where(eq(tables.issues.id, f.issueId))).toHaveLength(0);
const [removed] = await db.select().from(tables.mcpAttachmentUploads).where(eq(tables.mcpAttachmentUploads.id, String(upload.uploadId)));
expect(removed.attachmentId).toBe(result.body.attachment.id);
expect(removed.cleanupVerifiedAt !== null).toBe(!storageFailure);
expect((await p.storage.headObject(f.companyId, removed.objectKey)).exists).toBe(storageFailure);
expect(await blocked("mcp_attachment_uploads")).toBe(storageFailure);
} finally { await p.cleanup(); }
}, 60_000);
@@ -205,7 +332,7 @@ const support = externalDatabaseUrl ? { supported: true } : await getEmbeddedPos
operation: "paperclip_create_task", requestId: randomUUID(), argumentsHash: "a", status: "completed", result: { id: f.issueId } }).returning();
const [upload] = await db.insert(tables.mcpAttachmentUploads).values({ ...p, companyId: f.companyId, taskId: f.issueId,
requestId: randomUUID(), argumentsHash: "a", originalFilename: "history.txt", contentType: "text/plain", byteSize: 1,
sha256: "a".repeat(64), objectKey: "history/abandoned", storageProvider: "local_disk", expiresAt: now, cleanedAt: now }).returning();
sha256: "a".repeat(64), objectKey: "history/abandoned", storageProvider: "local_disk", expiresAt: now, cleanedAt: now, cleanupVerifiedAt: now }).returning();
expect(await blocked("mcp_mutation_receipts")).toBe(false);
expect(await blocked("mcp_attachment_uploads")).toBe(false);
expect(await read()).toEqual({ version: 1, backgroundWork: "none" });
@@ -281,6 +408,698 @@ const support = externalDatabaseUrl ? { supported: true } : await getEmbeddedPos
expect(await blocked("mcp_event_deliveries")).toBe(true);
});
it.each([false, true])("requires actual Speko configure completion after tool receipts, defaultsFailure=%s", async defaultsFailure => {
const { chatChannelService } = await import("../services/chat-channels.js");
const { instanceSettingsService } = await import("../services/instance-settings.js");
const f = await seed(), p = await mcp(f), oldKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
process.env.PAPERCLIP_SECRETS_MASTER_KEY = "a".repeat(64);
await db.insert(tables.companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: p.userId,
membershipRole: "owner", status: "active" });
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
await db.insert(tables.principalPermissionGrants).values({ companyId: f.companyId, principalType: "user", principalId: p.userId,
permissionKey: "agents:configure" });
const tools: Record<string, unknown>[] = [], hooks: Record<string, unknown>[] = [];
let failDefaults = defaultsFailure;
const chat = chatChannelService(db, { heartbeat: { wakeup: async () => { throw new Error("No call expected"); } },
publicBaseUrl: "https://history.example", webhookPublicBaseUrl: "https://history.example", fetch: async (input, init) => {
const pathname = new URL(String(input)).pathname, method = init?.method ?? "GET";
if (pathname === "/v1/agents/agent_test") {
if (method === "PATCH" && failDefaults) return new Response("Defaults failed", { status: 503 });
return Response.json({ id: "agent_test", organizationId: "org_test", name: "History voice" });
}
if (pathname.startsWith("/v1/agents/agent_test/tools")) {
if (method === "GET") return Response.json(tools);
const tool = { ...JSON.parse(String(init?.body)), id: pathname.split("/").length > 5 ? pathname.split("/").at(-1)! : `tool_${tools.length}` };
const index = tools.findIndex(old => old.id === tool.id); if (index < 0) tools.push(tool); else tools[index] = tool;
return Response.json({ id: tool.id });
}
if (pathname.startsWith("/v1/webhooks")) {
if (method === "GET") return Response.json({ data: hooks });
const hook = { ...JSON.parse(String(init?.body)), id: "hook_test", filterTags: {} }; hooks.splice(0, hooks.length, hook);
return Response.json({ id: hook.id });
}
throw new Error(`Unexpected Speko provider boundary ${method} ${pathname}`);
} });
try {
const endpoint = await chat.create(f.companyId, { provider: "speko", assignedAgentId: f.agentId }, p.userId);
let configureAttempts = 0;
const configure = () => chat.configure(endpoint.id, { action: configureAttempts++ === 0 ? "configure" : "reconnect",
credentials: { apiKey: "fixture-api-key", agentId: "agent_test", signingSecret: `whsec_${Buffer.alloc(32, 7).toString("base64")}` } }, p.userId);
if (defaultsFailure) {
await expect(configure()).rejects.toMatchObject({ status: 422 });
const partial = (await db.select().from(tables.chatActions)).filter(a => a.endpointId === endpoint.id);
expect(partial).toHaveLength(4);
expect(partial.every(a => a.status === "completed")).toBe(true);
for (const receipt of partial) expect(await blockedActionKind(receipt.kind)).toBe(true);
failDefaults = false;
}
await configure();
const receipts = (await db.select().from(tables.chatActions)).filter(a => a.endpointId === endpoint.id);
expect(receipts).toHaveLength(4);
expect(await blockedActionKind("speko_tool_setup")).toBe(false);
expect(await blockedActionKind("speko_lifecycle_setup")).toBe(false);
const [activity] = await db.select().from(tables.activityLog).where(eq(tables.activityLog.action,
defaultsFailure ? "chat_endpoint.reconnected" : "chat_endpoint.credentials_verified"));
expect(activity.details).toMatchObject({ endpointId: endpoint.id, provider: "speko" });
} finally {
await chat.shutdown();
if (oldKey === undefined) delete process.env.PAPERCLIP_SECRETS_MASTER_KEY; else process.env.PAPERCLIP_SECRETS_MASTER_KEY = oldKey;
}
}, 60_000);
it("retires actual GitHub omission navigation only after its exact native response publication settles", async () => {
const { createGitHubPublicationPolicy } = await import("../services/chat-channels/github/publication-policy.js");
const { ensureNativeCompletionContract } = await import("../services/native-runtime/completion-contracts.js");
const { chatChannelService } = await import("../services/chat-channels.js");
const f = await slackFixture();
const [endpoint] = await db.update(tables.chatEndpoints).set({ provider: "github" }).where(eq(tables.chatEndpoints.id, f.endpoint.id)).returning();
const contract = await ensureNativeCompletionContract({ db, companyId: f.companyId, issue: { id: f.issueId, title: "Omitted source", description: null }, actorId: "history" });
const [comment] = await db.insert(tables.issueComments).values({ companyId: f.companyId, issueId: f.issueId,
authorAgentId: f.agentId, createdByRunId: f.runId, body: "Source unavailable; see the task" }).returning();
await db.update(tables.heartbeatRuns).set({ runtimeMode: "native", nativeIssueId: f.issueId, completionContractId: contract.row.id,
resultJson: { presentationDecision: { commentId: comment.id } } }).where(eq(tables.heartbeatRuns.id, f.runId));
const [result] = await db.insert(tables.nativeRunResults).values({ companyId: f.companyId, issueId: f.issueId, runId: f.runId,
completionContractId: contract.row.id, schemaStatus: "accepted", serverFingerprint: randomUUID(), canonicalSha256: "a".repeat(64), resultJson: {} }).returning();
await db.insert(tables.nativeRunFinalizations).values({ companyId: f.companyId, issueId: f.issueId, runId: f.runId,
phase: "committed", resultId: result.id });
const [publication] = await db.insert(tables.chatPublications).values({ companyId: f.companyId, endpointId: endpoint.id,
conversationId: f.conversation.id, issueId: f.issueId, commentId: comment.id, idempotencyKey: randomUUID(), payload: { text: comment.body } }).returning();
const unused = async (): Promise<never> => { throw new Error("Unexpected GitHub publication boundary"); };
const policy = createGitHubPublicationPolicy(db, { isExplicitOperatorPublication: () => false, getTaskBaseUrl: () => "https://history.example",
// Exact current native-input authority is the named consumer boundary;
// the actual policy validates canonical owner rows and writes the marker.
failedChatRetrySource: async () => ({ endpointId: endpoint.id, conversationId: f.conversation.id,
sources: [{ omissionReasons: { download_unavailable: 1 } }] }) as never,
isExternalActionAuthorizationChange: () => false, FailedChatRetryPublicationReadError: class extends Error {}, endpointRecord: unused,
runIdFromMilestonePublication: () => null, fetchImpl: unused, stageTerminalReceiptReactionRemovals: unused,
runtimeContextForRecord: unused, scheduleMessageProcessing: unused, processReceiptReaction: unused });
const text = await db.transaction(tx => policy.githubCurrentOmissionPreparedText(tx, publication, endpoint));
expect(text).toContain("Open this Paperclip task");
const [receipt] = await db.select().from(tables.chatActions).where(eq(tables.chatActions.kind, "github_omission_navigation"));
expect(receipt).toMatchObject({ status: "processed", result: null });
expect(await blockedActionKind("github_omission_navigation")).toBe(true);
await db.update(tables.chatPublications).set({ payload: { ...publication.payload, text: String(text) }, state: "delivery_unknown" })
.where(eq(tables.chatPublications.id, publication.id));
const chat = chatChannelService(db, { heartbeat: { wakeup: unused }, fetch: unused });
try { await chat.resolvePublication(endpoint.id, publication.id, "mark_delivered", f.userId); }
finally { await chat.shutdown(); }
expect(await blockedActionKind("github_omission_navigation")).toBe(false);
await db.update(tables.chatActions).set({ payload: { ...receipt.payload, resultId: randomUUID() } }).where(eq(tables.chatActions.id, receipt.id));
expect(await blockedActionKind("github_omission_navigation")).toBe(true);
});
it("retires an actual Discord status-command receipt while preserving registration deadline work", async () => {
const { reconcileStoredDiscordCommandRegistration } = await import("../services/chat-discord-command-registration-store.js");
const { createDiscordCommands } = await import("../services/chat-channels/discord/commands.js");
const { chatCredentialMutationLease } = await import("../services/chat-credential-mutation-lease.js");
const f = await slackFixture(), applicationId = String(1_400_000_000_000_000_000n + BigInt(Date.now()) + BigInt(randomBytes(2).readUInt16BE())), guildId = "1457808928258658549", channelId = "333333333333333333";
const [endpoint] = await db.update(tables.chatEndpoints).set({ provider: "discord", botExternalId: applicationId,
providerAccountId: guildId, capabilities: { slashCommands: true } }).where(eq(tables.chatEndpoints.id, f.endpoint.id)).returning();
const [principal] = await db.update(tables.chatExternalPrincipals).set({ provider: "discord", externalId: "444444444444444444",
providerAccountId: guildId }).where(eq(tables.chatExternalPrincipals.id, f.principal.id)).returning();
const context = { generation: 1, credentialFingerprint: "c".repeat(64), localEpoch: 0, version: "fixture", discordGatewayOwned: true };
const scope = { companyId: f.companyId, endpointId: endpoint.id, applicationId, guildId };
const remote: Record<string, unknown>[] = [];
const registered = await reconcileStoredDiscordCommandRegistration(db, { scope, runtimeFence: { generation: context.generation, credentialFingerprint: context.credentialFingerprint }, botToken: "fixture-token",
authorize: async tx => {
expect((await tx.select().from(tables.chatEndpoints).where(eq(tables.chatEndpoints.id, endpoint.id)).for("update"))[0].status).toBe("active");
}, fetch: async (_url, init) => {
if ((init?.method ?? "GET") === "GET") return Response.json(remote);
const command = { ...JSON.parse(String(init?.body)), id: "888888888888888888", application_id: applicationId, version: "999999999999999999" };
remote.splice(0, remote.length, command); return Response.json(command);
} });
expect(registered.kind).toBe("registered");
const leaseKey = "discord_gateway", token = randomUUID();
await db.insert(tables.chatEndpointLeases).values({ companyId: f.companyId, endpointId: endpoint.id, leaseKey, token,
expiresAt: new Date(Date.now() + 60_000) });
const record = { endpoint, assignedAgentName: "Idle agent", connectionName: "Discord", applicationId: randomUUID(), credentialSecretRefs: [] };
const unused = async (): Promise<never> => { throw new Error("No Discord transport/control boundary expected"); };
const service = createDiscordCommands(db, { endpointRecord: async () => record, runtimeContextForRecord: () => context,
runtimeGeneration: () => 1, credentialFingerprint: () => context.credentialFingerprint, resolveCredentialRefs: unused,
fetchImpl: unused, withCredentialMutationLease: chatCredentialMutationLease(db), runtimeCallbackRecord: async () => record,
ensurePrincipal: async () => ({ principal, userId: f.userId }), runtimeCallbackEndpoint: async () => endpoint,
nonDirectDestinationAllowed: () => true, lockCurrentPrincipalAuthorization: async () => ({ allowed: true, userId: f.userId }),
getDiscordGatewayOwnership: () => ({ leaseKey, token }), discordGatewayRuntimeIsCurrent: () => true,
stageAuthorizedTaskControlPublication: unused, scheduleMessageProcessing: unused, processPendingPublications: unused });
const thread = `discord:${guildId}:${channelId}`;
const result = await service.handleDiscordNativeCommand({ endpointId: endpoint.id, provider: "discord", transport: "discord_gateway",
event: { command: "/paperclip status", text: "", channelId: thread, channel: { id: thread },
user: { userId: principal.externalId, isBot: false, isMe: false }, raw: { id: "777777777777777777", application_id: applicationId,
type: 2, version: 1, channel_id: channelId, channel: { id: channelId, type: 0 }, guild_id: guildId, context: 0,
authorizing_integration_owners: { "0": guildId }, data: { id: "888888888888888888", type: 1, name: "paperclip",
options: [{ type: 1, name: "status" }] }, user: { id: principal.externalId, username: "operator", bot: false } } } } as never, context);
expect(result).toMatchObject({ kind: "accepted" });
expect(await blockedActionKind("discord_native_command")).toBe(false);
expect(await blockedActionKind("discord_command_registration")).toBe(true);
const [receipt] = await db.select().from(tables.chatActions).where(eq(tables.chatActions.kind, "discord_native_command"));
await db.update(tables.chatActions).set({ result: { ...receipt.result, kind: "unknown" } }).where(eq(tables.chatActions.id, receipt.id));
expect(await blockedActionKind("discord_native_command")).toBe(true);
});
it.each(["live_photo_video", "heif_jpeg_preview"] as const)("retains actual Photon %s provenance after source-bound storage registration", async relationKind => {
const { createInboundAttachments } = await import("../services/chat-channels/inbound-attachments.js");
const { createInboundWakeups } = await import("../services/chat-channels/inbound/wakeups.js");
const { downloadPhotonAttachment } = await import("../services/photon/attachments.js");
const { issueService } = await import("../services/issues.js");
const { createStorageService } = await import("../storage/service.js");
const { createLocalDiskStorageProvider } = await import("../storage/local-disk-provider.js");
const f = await slackFixture(), directory = await mkdtemp(path.join(tmpdir(), "history-photon-provenance-"));
const [endpoint] = await db.update(tables.chatEndpoints).set({ provider: "imessage-photon" }).where(eq(tables.chatEndpoints.id, f.endpoint.id)).returning();
const [comment] = await db.insert(tables.issueComments).values({ companyId: f.companyId, issueId: f.issueId, body: "Source-bound photo" }).returning();
const providerEventId = randomUUID();
const [delivery] = await db.insert(tables.chatDeliveries).values({ companyId: f.companyId, endpointId: endpoint.id,
conversationId: f.conversation.id, principalId: f.principal.id, providerEventId, deduplicationKey: randomUUID(),
eventKind: "message", state: "processing", attempts: 1, normalizedEvent: { providerEventId, kind: "message" } }).returning();
const unused = async (): Promise<never> => { throw new Error("Unexpected Photon admission boundary"); };
const wakes = createInboundWakeups(db, { authorizeInboundWakeup: unused, authorizeInboundWorkStart: unused,
attachmentOmissionDetail: () => null, isExternalActionAuthorizationChange: () => false, redactError: String,
fetchImpl: unused, heartbeat: { wakeup: unused } });
try {
await db.transaction(tx => wakes.stageInboundWakeup(tx, { endpoint, deliveryId: delivery.id, conversation: f.conversation,
commentId: comment.id, principalId: f.principal.id, actorUserId: null }));
const storage = createStorageService(createLocalDiskStorageProvider(directory));
const bytes = relationKind === "heif_jpeg_preview"
? await readFile(new URL("./photon/fixtures/synthetic.heic", import.meta.url))
: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/6S8AAAAASUVORK5CYII=", "base64");
const type = relationKind === "heif_jpeg_preview" ? "image/heic" : "image/png";
let body = bytes, closed = false;
if (relationKind === "live_photo_video") {
const companion = Buffer.from("Actual bounded companion bytes"), locator = { kind: "photon_attachment" as const,
lineId: "line", chatGuid: "chat", messageGuid: "message", attachmentGuid: "attachment" };
const info = { guid: "attachment", totalBytes: bytes.length, mimeType: type, fileName: "photo.png", isSticker: false, isHidden: false };
const stream = { async *[Symbol.asyncIterator]() {
yield { type: "header", info, companionInfo: { kind: "live-photo-video", mimeType: "video/mp4", totalBytes: companion.length, fileName: "motion.mp4" } };
yield { type: "primaryChunk", data: bytes }; yield { type: "companionChunk", data: companion };
}, close: async () => { closed = true; } };
body = await downloadPhotonAttachment({ messages: { get: async () => ({ guid: "message", chatGuids: ["chat"], content: { attachments: [info] } }) },
attachments: { downloadStream: () => stream } } as never, "line", locator);
expect(closed).toBe(true);
}
const service = createInboundAttachments(db, { storage, authorizeInboundWakeup: async () => ({ delivery, conversation: f.conversation }),
lifecycleRuntimeFence: () => ({ generation: 1, credentialFingerprint: "c".repeat(64) }), sanitizeFilename: value => value ?? null,
issuesSvc: issueService(db), isExternalActionAuthorizationChange: () => false, redactError: String });
const result = await service.ingestAttachments({ endpoint, endpointRuntime: {} as never, deliveryId: delivery.id,
issueId: f.issueId, issueCommentId: comment.id, actorUserId: null,
attachments: [{ type: "image", name: relationKind === "heif_jpeg_preview" ? "photo.heic" : "photo.png", mimeType: type,
size: bytes.length, fetchData: async () => body }] });
expect(result.omissionReasons).toEqual({});
expect(result.storedIds).toHaveLength(2);
const receipt = (await db.select().from(tables.chatActions)).find(a => a.kind === (relationKind === "heif_jpeg_preview" ? "attachment_derivative" : "attachment_companion"))!;
expect(receipt).toMatchObject({ status: "processed", payload: { kind: relationKind } });
expect(await blockedActionKind(receipt.kind)).toBe(false);
expect(await blockedActionKind("inbound_wakeup")).toBe(true);
const assets = await db.select().from(tables.assets);
for (const asset of assets) expect((await storage.headObject(f.companyId, asset.objectKey)).exists).toBe(true);
const { chatChannelService } = await import("../services/chat-channels.js");
const chat = chatChannelService(db, { heartbeat: { wakeup: unused }, fetch: unused });
try {
await expect(chat.publishBoardMessage(endpoint.id, f.conversation.id, "Reuse an already bound attachment", randomUUID(), f.userId,
[result.storedIds[0]])).rejects.toMatchObject({ status: 409 });
expect(await blockedActionKind("board_send_rejected")).toBe(false);
} finally { await chat.shutdown(); }
await db.update(tables.chatActions).set({ payload: { ...receipt.payload, derivativeSha256: "0".repeat(64) } }).where(eq(tables.chatActions.id, receipt.id));
expect(await blockedActionKind(receipt.kind)).toBe(true);
await db.update(tables.chatActions).set({ payload: receipt.payload }).where(eq(tables.chatActions.id, receipt.id));
const [otherIssue] = await db.insert(tables.issues).values({ companyId: f.companyId, title: "Wrong lineage" }).returning();
await db.update(tables.issueAttachments).set({ issueId: otherIssue.id }).where(eq(tables.issueAttachments.id, String(receipt.payload.derivativeAttachmentId)));
expect(await blockedActionKind(receipt.kind)).toBe(true);
await db.update(tables.issueAttachments).set({ issueId: f.issueId }).where(eq(tables.issueAttachments.id, String(receipt.payload.derivativeAttachmentId)));
const { issueRoutes } = await import("../routes/issues.js");
const { createMcpApiDispatch } = await import("../services/public-mcp/capabilities.js");
const { Router } = await import("express");
const router = Router(); router.use(issueRoutes(db, storage));
const api = createMcpApiDispatch(router), actor = { type: "board", userId: f.userId, companyIds: [f.companyId], source: "session",
memberships: [{ companyId: f.companyId, membershipRole: "owner", status: "active" }] };
for (const id of result.storedIds) expect(await api({ actor } as never, "DELETE", `/attachments/${id}`)).toEqual({ ok: true });
expect(await db.select().from(tables.issueAttachments)).toHaveLength(0);
for (const asset of assets) expect((await storage.headObject(f.companyId, asset.objectKey)).exists).toBe(false);
expect(await blockedActionKind(receipt.kind)).toBe(false);
expect(await blockedActionKind("inbound_wakeup")).toBe(true);
} finally { wakes.unregisterDeferredWakeOwner(); await rm(directory, { recursive: true, force: true }); }
}, 60_000);
it("retires actual Photon interaction, retry and response-notice history with their canonical owners", async () => {
const { issueThreadInteractionService } = await import("../services/issue-thread-interactions.js");
const { recordPhotonPublicationRetry } = await import("../services/chat-channels/imessage-photon/publication-retry.js");
const { stagePhotonResponseNotice } = await import("../services/chat-channels/imessage-photon/response-notices.js");
const { chatChannelService } = await import("../services/chat-channels.js");
const f = await seed(), c = await connection(f);
await db.update(tables.issues).set({ status: "in_progress" }).where(eq(tables.issues.id, f.issueId));
const [endpoint] = await db.insert(tables.chatEndpoints).values({ companyId: f.companyId, connectionId: c.connectionId,
assignedAgentId: f.agentId, provider: "imessage-photon", publicId: randomUUID(), status: "active", publicationMode: "automatic" }).returning();
const [conversation] = await db.insert(tables.chatConversations).values({ companyId: f.companyId, endpointId: endpoint.id,
issueId: f.issueId, externalConversationId: "photon-personal", externalThreadId: "photon-personal", externalLabel: "Personal", state: "active" }).returning();
const interactions = issueThreadInteractionService(db);
const interaction = await interactions.create({ id: f.issueId, companyId: f.companyId }, { kind: "ask_user_questions",
continuationPolicy: "wake_assignee", payload: { version: 1, title: "Choose", submitLabel: "Continue", questions: [{
id: "choice", prompt: "Choose", selectionMode: "single", required: true, allowOther: false,
options: [{ id: "yes", label: "Yes" }, { id: "no", label: "No" }] }] } }, { agentId: f.agentId });
const [token] = await db.select().from(tables.chatActions);
expect(token).toMatchObject({ kind: "photon_interaction", status: "issued", conversationId: conversation.id });
expect(await blockedActionKind("photon_interaction")).toBe(true);
await interactions.answerQuestions({ id: f.issueId, companyId: f.companyId }, interaction.id,
{ answers: [{ questionId: "choice", optionIds: ["yes"] }] }, { userId: "local-board" });
expect(await blockedActionKind("photon_interaction")).toBe(false);
const [previousSource] = await db.select().from(tables.chatPublications);
const [source] = await db.update(tables.chatPublications).set({ state: "delivery_unknown" })
.where(eq(tables.chatPublications.id, previousSource.id)).returning();
await db.transaction(async tx => {
await recordPhotonPublicationRetry(tx, endpoint.id, source, "local-board");
await stagePhotonResponseNotice(tx, { publication: source, reference: String(token.payload.reference), questionIndex: 0,
key: `photon-response:${token.payload.reference}:1`, text: "Answer saved" });
});
expect(await blockedActionKind("photon_publication_retry")).toBe(true);
expect(await blockedActionKind("photon_response_notice")).toBe(true);
const unused = async (): Promise<never> => { throw new Error("No Photon network expected"); };
const service = chatChannelService(db, { heartbeat: { wakeup: unused }, fetch: unused });
try {
for (const publication of await db.select().from(tables.chatPublications)) {
await db.update(tables.chatPublications).set({ state: "delivery_unknown" }).where(eq(tables.chatPublications.id, publication.id));
await service.resolvePublication(endpoint.id, publication.id, "mark_delivered", "local-board");
}
expect(await blocked("chat_actions")).toBe(false);
await db.update(tables.chatActions).set({ payload: { ...token.payload, interactionId: randomUUID() } }).where(eq(tables.chatActions.id, token.id));
expect(await blockedActionKind("photon_interaction")).toBe(true);
} finally { await service.shutdown(); }
});
it("retains actual Slack connect discovery while its separate notice remains pending", async () => {
const { createSlackCommands, slackCommandForAgent } = await import("../services/chat-channels/slack/commands.js");
const { createProviderEffects } = await import("../services/chat-channels/provider-effects.js");
const { chatCredentialMutationLease } = await import("../services/chat-credential-mutation-lease.js");
const f = await slackFixture(), unused = async (): Promise<never> => { throw new Error("Unexpected discovery boundary"); };
const effects = createProviderEffects(db, { endpointRecord: unused, runtimeCallbackEndpoint: async () => f.endpoint,
withCredentialMutationLease: chatCredentialMutationLease(db), runtimeFor: unused, invalidateRuntime: unused,
lockCurrentPrincipalAuthorization: unused, redactError: String, MAX_ERROR_TEXT: 500,
nonDirectDestinationAllowed: () => false, lockTelegramCallbackNoticeAuthority: unused });
const command = slackCommandForAgent("Idle agent", f.endpoint.publicId), scheduled: string[] = [];
const service = createSlackCommands(db, { runtime: { get: () => undefined }, runtimeFor: unused, handleSdkMessage: unused,
stageProviderEffect: effects.stageProviderEffect, scheduleProviderEffect: id => { scheduled.push(id); },
ensurePrincipal: async () => ({ principal: f.principal, userId: null, linkedDenied: false }),
runtimeCallbackEndpoint: async () => f.endpoint, getPublicBaseUrl: () => "https://history.example",
canonicalProviderResourceId: unused, nonDirectDestinationAllowed: () => false, sponsorAllowsGuest: unused,
latestSlackDmControlThreadId: unused, scheduleMessageProcessing: unused, processSlackTaskStart: unused });
await service.handleSlackSlashCommand({ endpointId: f.endpoint.id, provider: "slack", transport: "slack_socket",
event: { command, text: "connect", triggerId: randomUUID(), channel: { id: "DCHANNEL" },
user: { userId: "UREQUESTER", isBot: false, isMe: false }, raw: {} } } as never,
{ generation: 1, credentialFingerprint: "c".repeat(64), localEpoch: 0, version: "fixture" },
{ endpoint: f.endpoint, assignedAgentName: "Idle agent", connectionName: "Slack", applicationId: randomUUID(), credentialSecretRefs: [] }, command, "connect");
expect(scheduled).toHaveLength(1);
expect((await db.select().from(tables.chatIdentityLinks))[0]).toMatchObject({ status: "pending", confirmationTokenHash: expect.any(String) });
expect(await blockedActionKind("slack_connect")).toBe(false);
expect(await blockedActionKind("provider_effect")).toBe(true);
expect(await blocked("chat_actions")).toBe(true);
});
it("retains the actual Telegram stop-subscription ownership receipt", async () => {
const { createTelegramMaintenance } = await import("../services/chat-channels/telegram/maintenance.js");
const { chatCredentialMutationLease } = await import("../services/chat-credential-mutation-lease.js");
const f = await seed(), c = await connection(f);
await db.update(tables.toolConnections).set({ enabled: true, status: "active" }).where(eq(tables.toolConnections.id, c.connectionId));
const [endpoint] = await db.insert(tables.chatEndpoints).values({ companyId: f.companyId, connectionId: c.connectionId,
assignedAgentId: f.agentId, provider: "telegram", publicId: randomUUID(), status: "active", botExternalId: "1234567" }).returning();
const unused = async (): Promise<never> => { throw new Error("Unexpected Telegram provider boundary"); };
const context = { generation: 3, credentialFingerprint: "c".repeat(64) }, withCredentialMutationLease = chatCredentialMutationLease(db);
const service = createTelegramMaintenance(db, { endpointRecord: unused, runtimeContextForRecord: () => context,
withCredentialMutationLease, resolveCredentials: unused, clearCredentials: unused,
runtimeCallbackEndpoint: async () => endpoint, getWebhookPublicBaseUrl: () => "https://history.example",
fetchImpl: unused, runtimeGeneration: () => 3, credentialFingerprint: () => context.credentialFingerprint,
redactError: String, credentialCheckTimeoutMs: 1000 });
await withCredentialMutationLease(endpoint, lease => service.recordTelegramStopSubscription(endpoint, context, lease));
expect(await service.hasTelegramStopSubscription(endpoint, context)).toBe(true);
expect(await blocked("chat_actions")).toBe(false);
const [receipt] = await db.select().from(tables.chatActions);
await db.update(tables.chatActions).set({ payload: { ...receipt.payload, webhookUrlSha256: "unknown" } }).where(eq(tables.chatActions.id, receipt.id));
expect(await blocked("chat_actions")).toBe(true);
});
it("retains the actual Slack private-source boundary and governed completed write", async () => {
const { recordSlackReadBoundary } = await import("../services/connectors/slack-access.js");
const { registerSlackTaskAuthority } = await import("../services/connectors/slack-authority.js");
const { executeSlackWrite } = await import("../services/connectors/slack-writes.js");
const f = await slackFixture();
await recordSlackReadBoundary(db, f.authority, { id: "CPRIVATE", is_private: true });
expect(await blocked("chat_actions")).toBe(false);
const invocationId = randomUUID();
await db.insert(tables.toolInvocations).values({ id: invocationId, companyId: f.companyId, agentId: f.agentId,
issueId: f.issueId, runId: f.runId, connectionId: f.connectionId, actorType: "agent", actorId: f.agentId,
toolName: "slack_post_message", status: "executing" });
const unregister = registerSlackTaskAuthority(db, async () => f.authority);
const methods: string[] = [];
try {
await executeSlackWrite(db, { companyId: f.companyId, agentId: f.agentId, issueId: f.issueId, runId: f.runId },
"slack_post_message", { channel: "DCHANNEL", text: "Confirmed message", idempotencyKey: randomUUID() }, invocationId,
async input => {
const method = new URL(String(input)).pathname.split("/").at(-1)!; methods.push(method);
if (method === "conversations.info") return Response.json({ ok: true, channel: { id: "DCHANNEL", is_im: true, user: "UREQUESTER" } });
if (method === "users.info") return Response.json({ ok: true, user: { id: "UREQUESTER", team_id: "TWORKSPACE", is_bot: false } });
if (method === "chat.postMessage") return Response.json({ ok: true, channel: "DCHANNEL", ts: "123.456" });
throw new Error(`Unexpected Slack boundary ${method}`);
});
expect(methods).toContain("chat.postMessage");
expect((await db.select().from(tables.chatActions)).find(a => a.kind === "slack_tool_write"))
.toMatchObject({ status: "processed", result: { channel: "DCHANNEL", ts: "123.456" } });
expect(await blocked("chat_actions")).toBe(false);
await db.update(tables.chatActions).set({ status: "uncertain" }).where(eq(tables.chatActions.kind, "slack_tool_write"));
expect(await blocked("chat_actions")).toBe(true);
} finally { unregister(); }
});
it("retires actual Slack invitation, test-message and mirror receipts without hiding their publication", async () => {
const { slackTestMessageService } = await import("../services/connectors/slack/test-message.js");
const { mirrorSlackBoardComment } = await import("../services/slack-board-messages.js");
const { chatChannelService } = await import("../services/chat-channels.js");
const { chatCredentialMutationLease } = await import("../services/chat-credential-mutation-lease.js");
const { instanceSettingsService } = await import("../services/instance-settings.js");
const f = await slackFixture();
await instanceSettingsService(db).updateExperimental({ enableChatConnectors: true });
const unused = async (): Promise<never> => { throw new Error("Unexpected provider boundary"); };
const credentialLease = chatCredentialMutationLease(db);
const send = slackTestMessageService(db, { withLock: (id, task) => credentialLease(f.endpoint, task),
runtimeBotToken: async () => "test-token", linkInstaller: async () => {}, ensureAccount: async () => f.principal.id,
createLinkIntent: async () => ({ confirmationUrl: "https://example.test/link", expiresAt: new Date(Date.now() + 600_000).toISOString() }),
fetch: async input => {
const method = new URL(String(input)).pathname.split("/").at(-1);
if (method === "users.info") return Response.json({ ok: true, user: { id: "UREQUESTER", team_id: "TWORKSPACE", is_bot: false } });
if (method === "chat.postMessage") return Response.json({ ok: true, channel: "DCHANNEL", ts: "123.456" });
throw new Error(`Unexpected setup boundary ${method}`);
} });
expect(await send(f.endpoint.id, { userId: f.userId }, { requestId: randomUUID(), slackUserId: "UREQUESTER" }))
.toMatchObject({ status: "connection_pending" });
expect(await blocked("chat_actions")).toBe(false);
await db.insert(tables.chatIdentityLinks).values({ companyId: f.companyId, endpointId: f.endpoint.id,
principalId: f.principal.id, paperclipUserId: f.userId, status: "linked", confirmedAt: now });
expect(await send(f.endpoint.id, { userId: f.userId }, { requestId: randomUUID(), slackUserId: "UREQUESTER" }))
.toMatchObject({ status: "sent" });
expect(await blocked("chat_actions")).toBe(false);
const [comment] = await db.insert(tables.issueComments).values({ companyId: f.companyId, issueId: f.issueId,
authorType: "user", authorUserId: f.userId, body: "Mirror this board comment" }).returning();
await db.transaction(tx => mirrorSlackBoardComment(tx, comment));
const [publication] = await db.select().from(tables.chatPublications);
expect(publication).toMatchObject({ state: "pending", commentId: comment.id });
expect(await blocked("chat_actions")).toBe(true);
const service = chatChannelService(db, { heartbeat: { wakeup: unused }, fetch: unused });
try {
await db.update(tables.chatPublications).set({ state: "delivery_unknown" }).where(eq(tables.chatPublications.id, publication.id));
await service.resolvePublication(f.endpoint.id, publication.id, "mark_delivered", f.userId);
expect(await blocked("chat_actions")).toBe(false);
await db.update(tables.chatActions).set({ status: "uncertain" }).where(eq(tables.chatActions.kind, "slack_test_message"));
expect(await blocked("chat_actions")).toBe(true);
} finally { await service.shutdown(); }
});
it("retires the actual shared GitHub response target only after its request is final", async () => {
const { githubIngressFixture } = await import("./helpers/github-webhook-ingress.js");
const { writeGitHubResponseComment } = await import("../services/chat-github-response-comments.js");
const { stagePullResponse } = await import("../services/chat-channels/github/response-target.js");
const { withGitHubPublicationLease } = await import("../services/chat-github-publication-lease.js");
const f = await githubIngressFixture(db), providerEventId = randomUUID();
const [issue] = await db.insert(tables.issues).values({ companyId: f.companyId, title: "GitHub reply", status: "done" }).returning();
const [conversation] = await db.insert(tables.chatConversations).values({ companyId: f.companyId, endpointId: f.endpoint.id,
issueId: issue.id, externalConversationId: "repository:418", externalThreadId: "github:org/repo:7", externalLabel: "PR7" }).returning();
const [delivery] = await db.insert(tables.chatDeliveries).values({ companyId: f.companyId, endpointId: f.endpoint.id,
conversationId: conversation.id, providerEventId, deduplicationKey: randomUUID(), eventKind: "action", normalizedEvent: { providerEventId, kind: "action" } }).returning();
const source = { endpoint: f.endpoint, conversation, delivery, number: 7, replyId: null, repositoryId: "418" };
await stagePullResponse(db, source);
expect(await blocked("chat_actions")).toBe(false); // initialized target owns no request yet
let posted: { id: number; html_url: string; body: string; user: { login: string | null } } | undefined;
const calls: string[] = [];
const api = { request: async <T>(pathname: string, options?: { method?: string; body?: unknown }): Promise<T> => {
calls.push(`${options?.method ?? "GET"} ${pathname}`);
if (!options?.method) return (pathname.includes("?per_page=") ? [] : posted) as T;
posted = { id: 123, html_url: "https://github.com/org/repo/pull/7#issuecomment-123",
body: (options.body as { body: string }).body, user: { login: f.endpoint.botUsername } };
return posted as T;
} };
const publish = (final: boolean) => withGitHubPublicationLease(db, { companyId: f.companyId, endpointId: f.endpoint.id,
repositoryId: "418", number: 7 }, async () => { throw new Error("No lease network expected"); }, lease =>
writeGitHubResponseComment(db, source, api, lease, { body: final ? "Final result" : "Working", final }, async () => {}));
await publish(false);
expect(await blocked("chat_actions")).toBe(true);
await publish(true);
expect(calls).toContain("PATCH /issues/comments/123");
expect(await blocked("chat_actions")).toBe(false);
const request = (await db.select().from(tables.chatActions)).find(a => a.kind === "github_response_comment")!;
expect(request).toMatchObject({ status: "processed", result: { final: true, id: "123" } });
await db.update(tables.chatActions).set({ result: { ...request.result, final: false } }).where(eq(tables.chatActions.id, request.id));
expect(await blocked("chat_actions")).toBe(true);
});
it("retires actual GitHub terminal review checks despite their retained retry hints", async () => {
const { githubIngressFixture } = await import("./helpers/github-webhook-ingress.js");
const { githubReviewCheckService } = await import("../services/chat-github-checks.js");
const { secretService } = await import("../services/secrets.js");
const { defaultGitHubReviewPolicy } = await import("@paperclipai/shared");
const oldKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
process.env.PAPERCLIP_SECRETS_MASTER_KEY = "a".repeat(64);
try {
const f = await githubIngressFixture(db);
const privateKey = generateKeyPairSync("rsa", { modulusLength: 2048,
privateKeyEncoding: { type: "pkcs8", format: "pem" }, publicKeyEncoding: { type: "spki", format: "pem" } }).privateKey;
const refs = [];
for (const [key, value] of Object.entries({ appId: "12", installationId: "34", privateKey })) {
const secret = await secretService(db).create(f.companyId, { name: `GitHub ${key}`, key, provider: "local_encrypted", value });
refs.push({ configPath: `credentials.${key}`, secretId: secret.id, versionSelector: "latest" as const });
await secretService(db).createBinding({ companyId: f.companyId, secretId: secret.id, targetType: "tool_connection",
targetId: f.endpoint.connectionId, configPath: `credentials.${key}`, versionSelector: "latest" });
}
await db.update(tables.toolConnections).set({ enabled: true, status: "active", credentialSecretRefs: refs })
.where(eq(tables.toolConnections.id, f.endpoint.connectionId));
await db.insert(tables.chatGitHubConfigurations).values({ companyId: f.companyId, endpointId: f.endpoint.id, updatedByUserId: "local-board",
configuration: { version: 1, toolsEnabled: true, responsibleUserId: "local-board", memberAccess: "all_linked", people: [],
repositories: {}, defaults: { ...defaultGitHubReviewPolicy(), ratingThreshold: 3 } } });
await db.insert(tables.chatEndpointResources).values({ companyId: f.companyId, endpointId: f.endpoint.id,
providerResourceId: "org/repo", type: "repository", label: "org/repo", enabled: true,
availability: "available", metadata: { providerRepositoryId: "418" } });
const event = { event: "opened" as const, deliveryId: randomUUID(), repositoryId: "418", repository: "org/repo", pullNumber: 7,
title: "Saved review", body: "", baseSha: "a".repeat(40), headSha: "b".repeat(40), baseBranch: "master",
author: { id: "42", login: "octocat", isBot: false }, sender: { id: "42", login: "octocat" }, draft: false, labels: [] };
const calls: string[] = [];
let stale = false;
const service = githubReviewCheckService(db, async input => {
const pathname = new URL(String(input)).pathname; calls.push(pathname);
if (pathname === "/app/installations/34/access_tokens") return Response.json({ token: "test-token" });
if (pathname === "/repos/org/repo/pulls/7") return Response.json({ head: { sha: stale ? "c".repeat(40) : event.headSha } });
if (pathname.endsWith("/check-runs")) return Response.json(pathname.includes("/commits/")
? { check_runs: [] } : { id: 123, html_url: "https://github.com/org/repo/runs/123" });
throw new Error(`Unexpected GitHub boundary ${pathname}`);
});
await service.enqueue(f.endpoint, event, false, "manual review required");
const [pending] = await db.select().from(tables.chatActions).where(eq(tables.chatActions.kind, "github_review_check"));
expect(await blocked("chat_actions")).toBe(true);
await service.processPending();
const [completed] = await db.select().from(tables.chatActions).where(eq(tables.chatActions.id, pending.id));
expect(completed, JSON.stringify({ result: completed.result, calls })).toMatchObject({ status: "processed", result: { state: "manual_required", retryable: true } });
expect(calls).toContain("/repos/org/repo/check-runs");
expect(await blocked("chat_actions")).toBe(false);
stale = true;
await service.enqueue(f.endpoint, { ...event, deliveryId: randomUUID() }, false, "stale head");
const next = (await db.select().from(tables.chatActions)).find(a => a.status === "received")!;
await service.processPending();
expect((await db.select().from(tables.chatActions).where(eq(tables.chatActions.id, next.id)))[0])
.toMatchObject({ status: "cancelled", result: { code: "stale_head" } });
expect(await blocked("chat_actions")).toBe(false);
await service.enqueue(f.endpoint, { ...event, deliveryId: randomUUID() }, false, "connection removed");
await db.update(tables.chatEndpointResources).set({ enabled: false }).where(eq(tables.chatEndpointResources.endpointId, f.endpoint.id));
const last = (await db.select().from(tables.chatActions)).find(a => a.status === "received")!;
await service.processPending();
expect((await db.select().from(tables.chatActions).where(eq(tables.chatActions.id, last.id)))[0])
.toMatchObject({ status: "cancelled", result: { code: "connection_changed" } });
expect(await blocked("chat_actions")).toBe(false);
await db.update(tables.chatActions).set({ result: { state: "queued", retryable: true } }).where(eq(tables.chatActions.id, completed.id));
expect(await blocked("chat_actions")).toBe(true);
} finally { if (oldKey === undefined) delete process.env.PAPERCLIP_SECRETS_MASTER_KEY; else process.env.PAPERCLIP_SECRETS_MASTER_KEY = oldKey; }
}, 60_000);
it("retires real question-form tokens only after their exact canonical answer commits", async () => {
const { issueThreadInteractionService } = await import("../services/issue-thread-interactions.js");
const { claimChatQuestionFormSubmission, completeChatQuestionFormSubmission } = await import("../services/chat-question-forms.js");
const f = await seed(), c = await connection(f);
await db.update(tables.issues).set({ status: "in_progress" }).where(eq(tables.issues.id, f.issueId));
const [endpoint] = await db.insert(tables.chatEndpoints).values({ companyId: f.companyId, connectionId: c.connectionId,
assignedAgentId: f.agentId, publicId: randomUUID(), provider: "slack", status: "active", publicationMode: "automatic",
capabilities: { actions: true, modals: true } }).returning();
const [conversation] = await db.insert(tables.chatConversations).values({ companyId: f.companyId, endpointId: endpoint.id,
issueId: f.issueId, externalConversationId: "question-channel", externalThreadId: "question-thread", externalLabel: "Question", state: "active" }).returning();
const [principal] = await db.insert(tables.chatExternalPrincipals).values({ companyId: f.companyId, provider: "slack",
providerAccountId: "workspace", externalId: "answering-user" }).returning();
const service = issueThreadInteractionService(db);
const interaction = await service.create({ id: f.issueId, companyId: f.companyId }, {
kind: "ask_user_questions", continuationPolicy: "wake_assignee", title: "Two questions",
payload: { version: 1, title: "Two questions", submitLabel: "Continue", questions: ["first", "second"].map(id => ({
id, prompt: id, selectionMode: "single" as const, required: true, allowOther: false,
options: [{ id: "yes", label: "Yes" }, { id: "no", label: "No" }] })) },
}, { agentId: f.agentId });
const actions = await db.select().from(tables.chatActions);
const open = actions.find(a => a.kind === "question_form_open"), submit = actions.find(a => a.kind === "question_form_submit");
expect(open).toMatchObject({ conversationId: conversation.id, status: "issued", result: null });
expect(submit).toMatchObject({ status: "issued", result: null });
expect(await blocked("chat_actions")).toBe(true);
await service.answerQuestions({ id: f.issueId, companyId: f.companyId }, interaction.id,
{ answers: ["first", "second"].map(questionId => ({ questionId, optionIds: ["yes"] })) }, { userId: "local-board" }, {
afterResolveInTransaction: async (tx, resolved) => {
expect(await claimChatQuestionFormSubmission(tx, { actionRowId: submit!.id, principalId: principal.id })).toBe(true);
expect(await completeChatQuestionFormSubmission(tx, { actionRowId: submit!.id, interactionId: resolved.id })).toBe(true);
},
});
expect((await db.select().from(tables.issueThreadInteractions).where(eq(tables.issueThreadInteractions.id, interaction.id)))[0])
.toMatchObject({ status: "answered", resolvedAt: expect.any(Date) });
expect(await blocked("chat_actions")).toBe(false);
await db.update(tables.chatActions).set({ result: { code: "question_form_answered", interactionId: randomUUID() } })
.where(eq(tables.chatActions.id, submit!.id));
expect(await blocked("chat_actions")).toBe(true);
});
it("retires a staged inbound wake only after its actual delivery owner rejects it", async () => {
const { githubIngressFixture } = await import("./helpers/github-webhook-ingress.js");
const { createInboundWakeups } = await import("../services/chat-channels/inbound/wakeups.js");
const { chatChannelService } = await import("../services/chat-channels.js");
const f = await githubIngressFixture(db);
const [issue] = await db.insert(tables.issues).values({ companyId: f.companyId, title: "Rejected input" }).returning();
const [conversation] = await db.insert(tables.chatConversations).values({ companyId: f.companyId, endpointId: f.endpoint.id,
issueId: issue.id, externalConversationId: "rejected", externalThreadId: "rejected", externalLabel: "Rejected" }).returning();
const [principal] = await db.insert(tables.chatExternalPrincipals).values({ companyId: f.companyId, provider: "github",
providerAccountId: "test", externalId: "42" }).returning();
const [comment] = await db.insert(tables.issueComments).values({ companyId: f.companyId, issueId: issue.id, body: "Input" }).returning();
const providerEventId = randomUUID();
const [delivery] = await db.insert(tables.chatDeliveries).values({ companyId: f.companyId, endpointId: f.endpoint.id,
conversationId: conversation.id, providerEventId, deduplicationKey: randomUUID(), eventKind: "message", state: "received",
normalizedEvent: { providerEventId, kind: "message" } }).returning();
const unused = async (): Promise<never> => { throw new Error("No provider/admission boundary expected"); };
const wakes = createInboundWakeups(db, { authorizeInboundWakeup: unused, authorizeInboundWorkStart: unused,
attachmentOmissionDetail: () => null, isExternalActionAuthorizationChange: () => false, redactError: String,
fetchImpl: unused, heartbeat: { wakeup: unused } });
const service = chatChannelService(db, { heartbeat: { wakeup: unused }, fetch: unused });
try {
await db.transaction(tx => wakes.stageInboundWakeup(tx, { endpoint: f.endpoint, deliveryId: delivery.id,
conversation, commentId: comment.id, principalId: principal.id, actorUserId: null }));
expect(await blocked("chat_actions")).toBe(true);
await wakes.settleRejectedInboundWakeups(delivery.id);
expect((await db.select().from(tables.chatActions))[0].status).toBe("preparing");
await service.configure(f.endpoint.id, { action: "pause" }, "local-board");
await wakes.settleRejectedInboundWakeups(delivery.id);
expect((await db.select().from(tables.chatActions))[0]).toMatchObject({ status: "failed",
result: { code: "inbound_wakeup_delivery_rejected" } });
expect(await db.select().from(tables.agentWakeupRequests)).toHaveLength(0);
expect(await blocked("chat_actions")).toBe(false);
expect(await blocked("chat_deliveries")).toBe(false);
await db.update(tables.chatDeliveries).set({ nextAttemptAt: new Date(Date.now() + 60_000) }).where(eq(tables.chatDeliveries.id, delivery.id));
expect(await blocked("chat_actions")).toBe(true);
} finally { wakes.unregisterDeferredWakeOwner(); await service.shutdown(); }
});
it("retires actual Teams consent and provider-effect receipts only after the transfer closes", async () => {
const { teamsFileTransferService } = await import("../services/chat-teams-file-transfers.js");
const { projectTeamsFilePublication } = await import("../services/chat-teams-file-publication.js");
const { installTeamsFileConsentHook, restoreTeamsFileConsentBinding } = await import("../services/chat-teams-file-consent.js");
const oldKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
process.env.PAPERCLIP_SECRETS_MASTER_KEY = "a".repeat(64);
try {
const f = await seed(), c = await connection(f), bytes = Buffer.from("Actual Teams transfer");
const authority = { companyId: f.companyId, endpointId: randomUUID(), conversationId: randomUUID(), issueId: f.issueId,
publicationId: randomUUID(), commentId: randomUUID(), attachmentId: randomUUID(), principalId: randomUUID(),
authorizedUserId: "local-board", runtimeGeneration: 3, credentialFingerprint: "c".repeat(64), conversationGeneration: 2,
sourceDigest: "d".repeat(64), tenantId: randomUUID(), botAppId: randomUUID(), aadObjectId: randomUUID(),
providerConversationId: "a:exact-personal", providerUserId: "29:exact-recipient",
sha256: createHash("sha256").update(bytes).digest("hex"), byteSize: bytes.length, filename: "file.txt" };
await db.insert(tables.chatEndpoints).values({ id: authority.endpointId, companyId: f.companyId, connectionId: c.connectionId,
assignedAgentId: f.agentId, publicId: randomUUID(), provider: "microsoft-teams", status: "active",
providerAccountId: authority.tenantId, botExternalId: authority.botAppId });
await db.insert(tables.chatConversations).values({ id: authority.conversationId, companyId: f.companyId,
endpointId: authority.endpointId, issueId: f.issueId, externalConversationId: authority.providerConversationId,
externalThreadId: `teams:${Buffer.from(authority.providerConversationId).toString("base64url")}`, externalLabel: "Personal",
isDirectMessage: true, sessionGeneration: 2 });
await db.insert(tables.chatExternalPrincipals).values({ id: authority.principalId, companyId: f.companyId, provider: "microsoft-teams",
providerAccountId: authority.tenantId, externalId: authority.aadObjectId });
await db.insert(tables.issueComments).values({ id: authority.commentId, companyId: f.companyId, issueId: f.issueId,
authorUserId: "local-board", body: "Share this file" });
const [asset] = await db.insert(tables.assets).values({ companyId: f.companyId, provider: "local_disk", objectKey: randomUUID(),
contentType: "text/plain", byteSize: bytes.length, sha256: authority.sha256, originalFilename: authority.filename }).returning();
await db.insert(tables.issueAttachments).values({ id: authority.attachmentId, companyId: f.companyId, issueId: f.issueId,
assetId: asset.id, issueCommentId: authority.commentId });
await db.insert(tables.chatPublications).values({ id: authority.publicationId, companyId: f.companyId,
endpointId: authority.endpointId, conversationId: authority.conversationId, issueId: f.issueId, commentId: authority.commentId,
idempotencyKey: randomUUID(), payload: { text: "Share file", attachmentIds: [authority.attachmentId] } });
const service = teamsFileTransferService(db, { authorize: async () => ({ ...authority }), loadBytes: async () => bytes,
postConsent: async () => ({ id: "card-1" }), postFileInfo: async () => ({ id: "file-info-1" }),
uploadRequest: async () => Response.json({ id: "item-1", name: "file.txt", size: bytes.length }, { status: 201 }),
project: projectTeamsFilePublication });
const created = await service.issue(authority, new Date(Date.now() + 60_000));
await service.process(f.companyId, created.id);
const [pending] = await db.select().from(tables.chatTeamsFileTransfers);
expect(pending.phase).toBe("awaiting_consent");
expect((await db.select().from(tables.chatActions)).map(a => a.kind)).toContain("teams_file_effect_intent");
expect(await blocked("chat_actions")).toBe(true);
const binding = await restoreTeamsFileConsentBinding({ companyId: f.companyId, endpointId: authority.endpointId,
transferId: created.id, authorityDigest: pending.authorityDigest }, pending.privateState.binding as Record<string, unknown>);
const handlers = new Map<string, (context: { activity: unknown }) => Promise<{ status: number }>>();
installTeamsFileConsentHook({ on: (name, callback) => { handlers.set(name, callback); } }, {
companyId: f.companyId, endpointId: authority.endpointId, tenantId: authority.tenantId, botAppId: authority.botAppId,
onConsent: event => service.recordConsent(event) });
expect((await handlers.get("file.consent.decline")!({ activity: { type: "invoke", name: "fileConsent/invoke", channelId: "msteams",
id: "decline-1", from: { id: authority.providerUserId, aadObjectId: authority.aadObjectId },
recipient: { id: `28:${authority.botAppId}` }, conversation: { id: authority.providerConversationId, conversationType: "personal", tenantId: authority.tenantId },
replyToId: "card-1", value: { action: "decline", context: { schema: binding.schema, token: binding.token, action: "decline" } } } })).status).toBe(200);
expect((await db.select().from(tables.chatTeamsFileTransfers))[0]).toMatchObject({ phase: "declined", attemptId: null, attemptExpiresAt: null });
expect((await db.select().from(tables.chatActions)).map(a => a.kind)).toEqual(expect.arrayContaining(["teams_file_effect_intent", "teams_file_consent"]));
expect(await blocked("chat_actions")).toBe(false);
expect(await blocked("chat_publications")).toBe(false);
const [receipt] = await db.select().from(tables.chatActions).where(eq(tables.chatActions.kind, "teams_file_consent"));
await db.update(tables.chatActions).set({ payload: { ...receipt.payload, receiptDigest: "wrong" } }).where(eq(tables.chatActions.id, receipt.id));
expect(await blocked("chat_actions")).toBe(true);
} finally { if (oldKey === undefined) delete process.env.PAPERCLIP_SECRETS_MASTER_KEY; else process.env.PAPERCLIP_SECRETS_MASTER_KEY = oldKey; }
});
it("measures both chat predicates against 10000 valid retained deliveries and 30000 completed actions", async () => {
const { githubIngressFixture } = await import("./helpers/github-webhook-ingress.js");
const { chatChannelService } = await import("../services/chat-channels.js");
const f = await githubIngressFixture(db, undefined, 256), providerEventId = randomUUID();
const [delivery] = await db.insert(tables.chatDeliveries).values({ companyId: f.companyId, endpointId: f.endpoint.id,
providerEventId, deduplicationKey: randomUUID(), eventKind: "message", state: "received",
normalizedEvent: { providerEventId, kind: "message" } }).returning();
const action = await f.insertAction({ deliveryId: delivery.id });
await f.service.processGitHubWebhookIngress(action.id);
const unused = async (): Promise<never> => { throw new Error("No external boundary expected"); };
const service = chatChannelService(db, { heartbeat: { wakeup: unused }, fetch: unused });
try { await service.configure(f.endpoint.id, { action: "pause" }, "local-board"); }
finally { await service.shutdown(); }
expect(await blocked("chat_deliveries")).toBe(false);
expect(await blocked("chat_actions")).toBe(false);
// Retention load clones a receipt produced above; it is measurement data,
// not a substitute for the producer regressions in the surrounding cases.
await db.execute(sql`INSERT INTO chat_deliveries (company_id, endpoint_id, provider_event_id, deduplication_key, event_kind,
state, normalized_event, processed_at)
SELECT ${f.companyId}::uuid, ${f.endpoint.id}::uuid, 'retained:' || n, 'retained:' || n, d.event_kind, d.state,
jsonb_set(d.normalized_event, '{providerEventId}', to_jsonb('retained:' || n)), d.processed_at
FROM chat_deliveries d CROSS JOIN generate_series(1, 9999) n WHERE d.id = ${delivery.id}::uuid`);
await db.execute(sql`INSERT INTO chat_actions (company_id, endpoint_id, delivery_id, kind, provider_action_id, payload, status, result)
SELECT a.company_id, a.endpoint_id, d.id, a.kind, 'retained:' || d.id || ':' || n, a.payload, a.status, a.result
FROM chat_actions a JOIN chat_deliveries d ON d.endpoint_id = a.endpoint_id CROSS JOIN generate_series(1, 3) n
WHERE a.id = ${action.id}::uuid AND d.id <> ${delivery.id}::uuid`);
await db.execute(sql`ANALYZE chat_deliveries`);
await db.execute(sql`ANALYZE chat_actions`);
const current = completedHistoryChecks.filter(c => ["chat_deliveries", "chat_actions"].includes(c.reason));
const baselinePath = process.env.PAPERCLIP_HISTORY_QUERY_BASELINE_PATH;
const baseline: { reason: string; query: string }[] = baselinePath ? JSON.parse(await readFile(baselinePath, "utf8")) : [];
const measurements = [];
for (const [revision, checks] of [["published_aaf55692", baseline], ["repair", current]] as const) {
for (const check of checks) {
const started = performance.now();
const plan = await db.transaction(async tx => {
await tx.execute(sql`SET LOCAL statement_timeout = '5s'`);
return tx.execute(sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT EXISTS (${sql.raw(check.query)}) AS blocked`);
});
measurements.push({ revision, reason: check.reason, wallMs: performance.now() - started, plan });
}
}
const evidence = { deliveries: 10000, actions: 29998, measurements };
if (process.env.PAPERCLIP_HISTORY_QUERY_PLANS_PATH) await writeFile(process.env.PAPERCLIP_HISTORY_QUERY_PLANS_PATH,
JSON.stringify(evidence, null, 2));
console.info(`Retained chat history query plans ${JSON.stringify(evidence)}`);
expect(await blocked("chat_deliveries")).toBe(false);
expect(await blocked("chat_actions")).toBe(false);
}, 60_000);
it("uses actual chat ingress, lifecycle application and operator publication receipts", async () => {
const { githubIngressFixture } = await import("./helpers/github-webhook-ingress.js");
const { chatCredentialMutationLease } = await import("../services/chat-credential-mutation-lease.js");
@@ -119,6 +119,32 @@ describe("local disk storage provider", () => {
await expect(service.getObject("company-1", stored.objectKey)).rejects.toMatchObject({ status: 404 });
});
it.each(["EACCES", "EIO"])("retains storage uncertainty for %s instead of reporting absence", async code => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-storage-"));
tempRoots.push(root);
const service = createStorageService(createLocalDiskStorageProvider(root));
const stored = await service.putFile({ companyId: "company-1", namespace: "issues/issue-1", originalFilename: "retained.txt",
contentType: "text/plain", body: Buffer.from("must not disappear") });
const failure = Object.assign(new Error("controlled filesystem failure"), { code });
const unlink = vi.spyOn(fs, "unlink").mockRejectedValueOnce(failure);
try { await expect(service.deleteObject("company-1", stored.objectKey)).rejects.toBe(failure); }
finally { unlink.mockRestore(); }
expect(await service.headObject("company-1", stored.objectKey)).toMatchObject({ exists: true });
const stat = vi.spyOn(fs, "stat").mockRejectedValueOnce(failure);
try { await expect(service.headObject("company-1", stored.objectKey)).rejects.toBe(failure); }
finally { stat.mockRestore(); }
expect(await readStreamToBuffer((await service.getObject("company-1", stored.objectKey)).stream)).toEqual(Buffer.from("must not disappear"));
});
it("treats a non-directory object path as absent", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-storage-"));
tempRoots.push(root);
await fs.writeFile(path.join(root, "company-1"), "not a directory");
const service = createStorageService(createLocalDiskStorageProvider(root));
expect(await service.headObject("company-1", "company-1/object")).toEqual({ exists: false });
await expect(service.deleteObject("company-1", "company-1/object")).resolves.toBeUndefined();
});
it.each(["headObject", "deleteObject"] as const)("cancels %s even if a provider stops responding", async method => {
const provider = createLocalDiskStorageProvider(os.tmpdir());
const request = vi.spyOn(provider, method).mockImplementation(async () => new Promise<never>(() => {}));
@@ -1,3 +1,5 @@
import { chatHistoryActionBlocks } from "./helpers/chat-history-proof.js";
import { voiceCallReports } from "../services/voice/voice-call-reports.js";
import { subscribeDatabaseWork } from "@paperclipai/db";
import * as voiceReplyNotifications from "../services/voice/voice-reply-notifications.js";
import { voiceReplySchedule } from "../services/voice/voice-reply-schedule.js";
@@ -40,15 +42,16 @@ import { buildPaperclipRuntimeMcpServers } from "../services/heartbeat.js";
import { createToolGatewayService } from "../services/tool-gateway.js";
import { initializeRunIdentity } from "../services/run-identity.js";
import { syncSpekoVoiceTools, spekoToolsForSession, executeSpekoVoiceTool } from "../services/voice/speko-agent-tools.js";
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_VOICE_TEST_DATABASE_URL");
const historyExternal = resolveExternalTestDatabaseUrl("PAPERCLIP_HISTORY_TEST_DATABASE_URL");
const external = historyExternal ?? resolveExternalTestDatabaseUrl("PAPERCLIP_VOICE_TEST_DATABASE_URL");
const support = external ? { supported: true } : await getEmbeddedPostgresTestSupport();
(support.supported ? describe : describe.skip)("durable Speko voice sessions", () => {
let db: ReturnType<typeof createDb>;
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | undefined;
beforeAll(async () => {
if (!external) database = await startEmbeddedPostgresTestDatabase("paperclip-speko-store-");
if (external) await applyPendingMigrations(external);
db = createDb(external ?? database!.connectionString, { idleTimeoutSeconds: 0, maxLifetimeSeconds: 0 });
if (external && !historyExternal) await applyPendingMigrations(external);
db = createDb(external ?? database!.connectionString, { idleTimeoutSeconds: 0, maxLifetimeSeconds: 0, ...(historyExternal ? { maxConnections: 4 } : {}) });
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterAll(async () => { await db?.$client.end({ timeout: 1 }); await database?.cleanup(); });
@@ -75,7 +78,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
return { companyId, endpointId, sessionId, token: reserved.token, envelope, webhookId: `msg_${toolId}`, fingerprint: createHash("sha256").update(JSON.stringify(envelope)).digest("hex"), accept };
};
async function publication(state: "streaming" | "published" = "published") {
const [row] = await db.insert(chatPublications).values({ companyId, endpointId, conversationId, issueId, idempotencyKey: randomUUID(), state, payload: { text: "Approved result" } }).returning();
const [row] = await db.insert(chatPublications).values({ companyId, endpointId, conversationId, issueId, idempotencyKey: randomUUID(), state, publishedAt: state === "published" ? new Date() : null, payload: { text: "Approved result" } }).returning();
return row;
}
return { companyId, endpointId, agentId, connectionId, issueId, conversationId, callerId, sessionId, store, request, reserved, accept, toolInput, publication };
@@ -164,6 +167,13 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
const report = f.sign({ type: "call.report", session_id: f.call.providerSessionId, organization_id: f.event.organization_id });
await f.service.inbound.lifecycle(f.endpoint.publicId, report.body, report.headers);
await settle(); expect(f.transport.callReport).toHaveBeenCalledTimes(2); expect(onError).not.toHaveBeenCalled();
const receipts = (await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId)))
.filter(row => row.kind === "speko_lifecycle_receipt");
expect(receipts.length).toBeGreaterThan(0);
for (const receipt of receipts) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(true);
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.companyId, f.companyId));
expect(await voiceCallReports(db, f.transport.callReport).load({ kind: "company", id: f.companyId })).toEqual([]);
for (const receipt of receipts) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(false);
} finally {
await coordinator.stop(); vi.useRealTimers();
const ended = f.sign({ type: "call.status", call_id: f.call.providerSessionId, status: "ended" });
@@ -1381,7 +1391,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
it("sends long approved phone answers as durable ordered parts and resumes after a rate limit", async () => {
const f = await pushFixture(), text = "😀 answer ".repeat(4000) + "FINAL";
await db.update(chatPublications).set({payload: {text}}).where(eq(chatPublications.id, f.publication.id));
const [later] = await db.insert(chatPublications).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", payload: {text: "Later answer B"}}).returning();
const [later] = await db.insert(chatPublications).values({companyId: f.companyId, endpointId: f.endpointId, conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published", publishedAt: new Date(), payload: {text: "Later answer B"}}).returning();
await f.store.enqueuePublication(f.companyId, later.id);
f.transport.sendCallMessage.mockResolvedValueOnce({messageId: "part-0"}).mockRejectedValueOnce(new SpekoProviderError("provider_unavailable", false, 429));
await f.push();
@@ -1405,6 +1415,13 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
for (const call of calls) {expect(call[1].length).toBeLessThanOrEqual(16_000); expect(/^[\uDC00-\uDFFF]|[\uD800-\uDBFF]$/.test(call[1])).toBe(false);}
const [delivered] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.id, reply.id));
expect(delivered.deliveredAt).toBeInstanceOf(Date); expect(delivered.spokenAt).toBeNull();
const accepted = (await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId)))
.filter(row => row.kind === "speko_voice_reply_push");
for (const receipt of accepted) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(true);
await f.service.end(f.companyId, f.session.id, f.input.caller);
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.companyId, f.companyId));
expect(await voiceCallReports(db, f.transport.callReport).load({ kind: "company", id: f.companyId })).toEqual([]);
for (const receipt of accepted) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(false);
});
it("does not retry an uncertain long-answer part or send its suffix", async () => {
const f = await pushFixture();
@@ -1416,6 +1433,13 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
expect(f.transport.sendCallMessage).toHaveBeenCalledTimes(2);
const [reply] = await db.select().from(chatVoiceReplies).where(eq(chatVoiceReplies.publicationId, f.publication.id));
expect(reply.deliveredAt).toBeNull();
await f.service.end(f.companyId, f.session.id, f.input.caller);
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.companyId, f.companyId));
expect(await voiceCallReports(db, f.transport.callReport).load({ kind: "company", id: f.companyId })).toEqual([]);
const uncertain = (await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId)))
.filter(row => row.kind === "speko_voice_reply_push" && row.status === "unknown");
expect(uncertain).toHaveLength(1);
expect(await chatHistoryActionBlocks(db, uncertain[0]!.id)).toBe(true);
});
it("pushes a published phone answer exactly once across concurrent dispatch and restart", async () => {
const f = await pushFixture();
@@ -2027,6 +2051,15 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
expect(tools).toHaveLength(3);
expect(f.transport.configureTool).toHaveBeenCalledTimes(7);
expect(f.transport.configureTool.mock.calls.slice(-3).every((call) => Boolean(call[3]))).toBe(true);
const receipts = (await db.select().from(chatActions).where(eq(chatActions.endpointId, f.endpointId)))
.filter(row => ["speko_tool_setup", "speko_lifecycle_setup"].includes(row.kind));
expect(receipts).toHaveLength(4);
for (const receipt of receipts) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(true); // real24h refresh obligation
await db.update(chatVoiceSessions).set({ endedAt: new Date(Date.now() - 2 * 86_400_000) }).where(eq(chatVoiceSessions.companyId, f.companyId));
expect(await voiceCallReports(db, f.transport.callReport).load({ kind: "company", id: f.companyId })).toEqual([]);
// The resource helper alone has no post-configuration commit receipt;
// defaults/runtime completion belongs to the ordinary configure owner.
for (const receipt of receipts) expect(await chatHistoryActionBlocks(db, receipt.id)).toBe(true);
});
it.each(["current", "legacy"])("moves only receipt-owned %s tools and hooks when callback origin and signing secret change", async proofVersion => {
@@ -4,3 +4,5 @@ export { withExternalAdmissionGuard, withExternalExecutionAdmission, assertNoExt
export { dotRunnerBroker, createDotRunnerMcpTools } from "./dot.js";
export { notifyExternalAgentSettlement, pendingMuseTerminalBindings, type ExternalAgentSettlementScope } from "./muse-terminal-settlement.js";
export { idleExternalAgentHistoryChecks } from "./idle-history-proof.js";
+5 -1
View File
@@ -1,3 +1,4 @@
import { confirmMcpAttachmentCleanup, recordMcpAttachmentCleanup, type McpAttachmentCleanup } from "../services/public-mcp/upload-cleanup.js";
import { notifyDeferredExecutionWork } from "../services/deferred-execution-notifications.js";
import { notifyNativeRecoveryNotices } from "../services/native-recovery-notice-notifications.js";
import { notifyNativeStatusReassessment } from "../services/native-status-reassessment-notifications.js";
@@ -16254,6 +16255,7 @@ export function issueRoutes(
for (const attachment of attachments) {
try {
await storage.deleteObject(attachment.companyId, attachment.objectKey);
await recordMcpAttachmentCleanup(db, await confirmMcpAttachmentCleanup(db, storage, attachment));
} catch (err) {
logger.warn(
{ err, issueId: id, attachmentId: attachment.id },
@@ -20138,8 +20140,10 @@ export function issueRoutes(
if (!(await assertDeliverableMutationAllowedByRunContext(req, res, issue)))
return;
let mcpCleanup: McpAttachmentCleanup | null = null;
try {
await storage.deleteObject(attachment.companyId, attachment.objectKey);
mcpCleanup = await confirmMcpAttachmentCleanup(db, storage, attachment);
} catch (err) {
logger.warn(
{ err, attachmentId },
@@ -20147,7 +20151,7 @@ export function issueRoutes(
);
}
const removed = await svc.removeAttachment(attachmentId);
const removed = await svc.removeAttachment(attachmentId, { mcpCleanup });
if (!removed) {
res.status(404).json({ error: "Attachment not found" });
return;
@@ -0,0 +1,25 @@
/** Command acceptance is final history; a staged control publication remains an
* independent obligation and must settle before its command receipt is quiet. */
export function completedDiscordAction(a: string): string {
return `${a}.kind = 'discord_native_command' AND ${a}.status = 'processed'
AND ${a}.payload->>'version' = '1' AND ${a}.payload->'invocation'->>'schema' = 'paperclip.discord.native-command.v1'
AND ${a}.payload->'invocation'->>'command' IN ('status', 'new', 'close')
AND ${a}.payload->'invocation'->>'digest' ~ '^[a-f0-9]{64}$'
AND ${a}.provider_action_id = 'discord-native-command:' || (${a}.payload->'invocation'->>'interactionId')
AND ${a}.result->>'kind' = 'discord_native_command_recorded' AND length(${a}.result->>'content') > 0
AND EXISTS (SELECT 1 FROM chat_external_principals principal WHERE principal.id = ${a}.principal_id
AND principal.company_id = ${a}.company_id AND principal.provider = 'discord'
AND principal.external_id = ${a}.payload->'invocation'->>'actorExternalId')
AND ((${a}.conversation_id IS NULL AND ${a}.payload->'target' = 'null'::jsonb)
OR EXISTS (SELECT 1 FROM chat_conversations conversation WHERE conversation.id = ${a}.conversation_id
AND conversation.company_id = ${a}.company_id AND conversation.endpoint_id = ${a}.endpoint_id
AND conversation.id::text = ${a}.payload->'target'->>'conversationId'
AND conversation.issue_id::text = ${a}.payload->'target'->>'issueId'
AND conversation.session_generation::text = ${a}.payload->'target'->>'sessionGeneration'))
AND (${a}.result->'publicationId' = 'null'::jsonb OR EXISTS (
SELECT 1 FROM chat_publications publication WHERE publication.id::text = ${a}.result->>'publicationId'
AND publication.company_id = ${a}.company_id AND publication.endpoint_id = ${a}.endpoint_id
AND publication.conversation_id = ${a}.conversation_id
AND ((publication.state = 'published' AND publication.published_at IS NOT NULL)
OR (publication.state IN ('failed', 'cancelled') AND publication.next_attempt_at IS NULL))))`;
}
@@ -1,9 +1,65 @@
function completedIngress(a: string): string {
return `(${a}.status = 'processed' AND ${a}.result->>'httpStatus' ~ '^2[0-9][0-9]$' AND ${a}.result->>'retryable' = 'false')
OR (${a}.status = 'failed' AND ${a}.result->>'retryable' = 'false' AND (
${a}.result->>'code' = 'github_webhook_ingress_payload_invalid'
OR ${a}.result->>'httpStatus' ~ '^4[0-9][0-9]$'
OR (jsonb_typeof(${a}.result->'attempts') = 'number' AND (${a}.result->>'attempts')::numeric >= 5)))
OR (${a}.status = 'cancelled' AND ${a}.result->>'retryable' = 'false'
AND ${a}.result->>'code' IN ('github_webhook_ingress_runtime_superseded', 'github_webhook_recovery_source_changed'))`;
}
export function completedGithubAction(a: string): string {
return `(${a}.kind = 'github_response_comment' AND ${a}.status = 'processed' AND ${a}.result->>'final' = 'true')
OR (${a}.kind = 'github_webhook_ingress' AND (
(${a}.status = 'processed' AND ${a}.result->>'httpStatus' = '202' AND ${a}.result->>'retryable' = 'false')
OR (${a}.status = 'failed' AND ${a}.result->>'code' = 'github_webhook_ingress_payload_invalid'
AND ${a}.result->>'retryable' = 'false')))
OR (${a}.kind = 'github_review_check' AND ${a}.status = 'processed'
AND ${a}.result->>'retryable' = 'false')`;
return `(${a}.kind = 'github_omission_navigation' AND ${a}.status = 'processed' AND ${a}.result IS NULL
AND ${a}.payload->>'version' = '1' AND ${a}.payload->>'preparedTextSha256' ~ '^[a-f0-9]{64}$'
AND ${a}.provider_action_id = 'github-omission-navigation:' || (${a}.payload->>'publicationId')
AND EXISTS (SELECT 1 FROM chat_publications publication JOIN heartbeat_runs run
ON run.id::text = ${a}.payload->>'runId' AND run.company_id = publication.company_id
AND run.native_issue_id = publication.issue_id AND run.runtime_mode = 'native'
JOIN native_run_finalizations finalization ON finalization.run_id = run.id AND finalization.company_id = run.company_id
AND finalization.issue_id = run.native_issue_id AND finalization.result_id::text = ${a}.payload->>'resultId' AND finalization.phase = 'committed'
WHERE publication.id::text = ${a}.payload->>'publicationId' AND publication.company_id = ${a}.company_id
AND publication.endpoint_id = ${a}.endpoint_id AND publication.conversation_id = ${a}.conversation_id
AND run.status = 'succeeded' AND run.finished_at IS NOT NULL AND run.cost_accounting_pending = false
AND ((publication.state = 'published' AND publication.published_at IS NOT NULL)
OR (publication.state IN ('failed', 'cancelled') AND publication.next_attempt_at IS NULL))))
OR (${a}.kind = 'github_response_comment' AND ${a}.result->>'final' = 'true'
AND (${a}.status = 'processed' OR (${a}.status = 'cancelled' AND ${a}.result->>'code'
IN ('run_already_answered', 'passive_response_wait', 'response_deleted'))))
OR (${a}.kind = 'github_pr_response_comment' AND ${a}.status = 'processed'
AND (${a}.result IS NULL OR (${a}.result->>'final' = 'true' AND length(${a}.result->>'id') > 0
AND length(${a}.result->>'url') > 0))
AND EXISTS (SELECT 1 FROM chat_conversations conversation WHERE conversation.id = ${a}.conversation_id
AND conversation.company_id = ${a}.company_id AND conversation.endpoint_id = ${a}.endpoint_id)
AND NOT EXISTS (SELECT 1 FROM chat_actions request WHERE request.kind = 'github_response_comment'
AND request.company_id = ${a}.company_id AND request.endpoint_id = ${a}.endpoint_id
AND request.conversation_id = ${a}.conversation_id AND (request.result->>'final' = 'true') IS NOT TRUE))
OR (${a}.kind = 'github_webhook_ingress' AND (${completedIngress(a)}))
OR (${a}.kind = 'github_review_check' AND (
(${a}.status = 'processed' AND (${a}.result->>'state' IN ('manual_required', 'incomplete', 'error')
OR (${a}.result->>'code' = 'assessment_owns_check' AND EXISTS (
SELECT 1 FROM chat_github_reviews review WHERE review.id::text = ${a}.result->>'reviewId'
AND review.company_id = ${a}.company_id AND review.endpoint_id = ${a}.endpoint_id AND review.assessment IS NOT NULL))))
OR (${a}.status = 'cancelled' AND ${a}.result->>'code' IN ('connection_changed', 'stale_head')
AND (${a}.result->>'code' <> 'stale_head' OR NOT EXISTS (
SELECT 1 FROM chat_github_reviews review JOIN chat_deliveries delivery
ON delivery.id::text = review.delivery_id AND delivery.endpoint_id = review.endpoint_id
WHERE review.company_id = ${a}.company_id AND review.endpoint_id = ${a}.endpoint_id
AND review.state IN ('queued', 'running') AND review.assessment IS NULL
AND delivery.normalized_event->'githubAutomatic'->'context'->>'deliveryId' = ${a}.payload->'event'->>'deliveryId')))))
OR (${a}.kind = 'github_review_publication' AND (
(${a}.status = 'processed' AND (
(${a}.payload->>'operation' IN ('comment', 'update_comment', 'formal_review')
AND length(${a}.result->>'id') > 0 AND length(${a}.result->>'url') > 0)
OR (${a}.payload->>'operation' = 'assessment' AND EXISTS (SELECT 1 FROM chat_github_reviews review
WHERE review.id::text = ${a}.payload->>'reviewId' AND review.id::text = ${a}.result->>'reviewId'
AND review.company_id = ${a}.company_id AND review.endpoint_id = ${a}.endpoint_id
AND review.assessment IS NOT NULL AND review.check_url = ${a}.result->>'checkUrl'))))
OR (${a}.status = 'cancelled' AND ${a}.result->>'retryable' = 'false'
AND ${a}.result->>'code' IN ('publication_binding_unavailable', 'stale_head', 'authorization_changed'))))
OR (${a}.kind = 'github_webhook_recovery' AND (
(${a}.status = 'cancelled' AND ${a}.result->>'code' IN ('not_eligible_for_recovery', 'source_changed_or_unavailable'))
OR (${a}.status = 'processed' AND EXISTS (SELECT 1 FROM chat_actions ingress
WHERE ingress.kind = 'github_webhook_ingress' AND ingress.company_id = ${a}.company_id
AND ingress.endpoint_id = ${a}.endpoint_id AND ingress.payload->>'deliveryId' = ${a}.payload->'original'->>'guid'
AND (${completedIngress('ingress')})))))`;
}
@@ -1,6 +1,11 @@
import { finishedIdleRun, finishedIdleWake } from "../idle-work-proof.js";
import { completedGithubAction } from "./github/idle-history-proof.js";
import { completedSlackAction } from "./slack/idle-history-proof.js";
import { completedExpiredChatControl, completedQuestionFormAction } from "../chat-question-form-idle-proof.js";
import { completedPhotonAction } from "./imessage-photon/idle-history-proof.js";
import { completedDiscordAction } from "./discord/idle-history-proof.js";
import { completedVoiceAction } from "../voice/idle-history-proof.js";
import { completedTeamsAction } from "./microsoft-teams/idle-history-proof.js";
import { completedTelegramAction } from "./telegram/idle-history-proof.js";
/** A processed inbound receipt can still own an unconsumed wake. */
@@ -8,12 +13,18 @@ function completedAction(a: string): string {
const wake = `EXISTS (SELECT 1 FROM agent_wakeup_requests w WHERE w.company_id = ${a}.company_id
AND w.id = ${a}.id AND w.agent_id::text = ${a}.payload->>'agentId'
AND w.payload->>'issueId' = ${a}.payload->>'issueId' AND ${finishedIdleWake('w')})`;
return `jsonb_typeof(${a}.payload) = 'object' AND jsonb_typeof(${a}.result) = 'object'
return `jsonb_typeof(${a}.payload) = 'object'
AND EXISTS (SELECT 1 FROM chat_endpoints e WHERE e.id = ${a}.endpoint_id AND e.company_id = ${a}.company_id)
AND (${a}.delivery_id IS NULL OR EXISTS (SELECT 1 FROM chat_deliveries d
WHERE d.id = ${a}.delivery_id AND d.company_id = ${a}.company_id AND d.endpoint_id = ${a}.endpoint_id))
AND CASE
WHEN ${a}.kind = 'inbound_wakeup' THEN ${a}.status IN ('processed', 'failed') AND ${wake}
WHEN ${a}.kind = 'inbound_wakeup' THEN (${a}.status IN ('processed', 'failed') AND ${wake})
OR (${a}.status = 'failed' AND ${a}.result->>'code' = 'inbound_wakeup_delivery_rejected'
AND EXISTS (SELECT 1 FROM chat_deliveries rejected WHERE rejected.id = ${a}.delivery_id
AND rejected.company_id = ${a}.company_id AND rejected.endpoint_id = ${a}.endpoint_id
AND rejected.state IN ('filtered', 'failed') AND rejected.next_attempt_at IS NULL))
WHEN ${a}.kind IN ('question_form_open', 'question_form_submit', 'question_form_open_attempt', 'question_answer')
THEN (${completedQuestionFormAction(a)})
WHEN ${a}.kind = 'interaction_wakeup' THEN ${a}.status = 'processed' AND
(${a}.result->>'code' = 'interaction_wakeup_no_longer_applicable' OR EXISTS (
SELECT 1 FROM agent_wakeup_requests w WHERE w.company_id = ${a}.company_id
@@ -28,40 +39,64 @@ function completedAction(a: string): string {
WHEN ${a}.kind = 'receipt_reaction' THEN
(${a}.status = 'processed' AND jsonb_typeof(${a}.result->'attempts') = 'number' AND (${a}.result->>'attempts')::numeric > 0)
OR (${a}.status = 'cancelled' AND ${a}.result->>'code' IN ('receipt_reaction_superseded_by_terminal_publication', 'receipt_reaction_runtime_superseded'))
WHEN ${a}.kind = 'confirmation_response' THEN ${a}.status = 'processed'
WHEN ${a}.kind = 'confirmation_response' THEN (${completedExpiredChatControl(a)}) OR (${a}.status = 'processed'
AND ${a}.result->>'code' IN ('confirmation_resolution_committed_by_provider', 'confirmation_resolution_reconciled')
AND EXISTS (SELECT 1 FROM issue_thread_interactions interaction WHERE interaction.id::text = ${a}.result->>'interactionId'
AND interaction.company_id = ${a}.company_id AND interaction.status = ${a}.result->>'interactionStatus'
AND interaction.resolved_at IS NOT NULL)
AND interaction.resolved_at IS NOT NULL))
WHEN ${a}.kind = 'board_send_rejected' THEN ${a}.status = 'processed'
AND ${a}.result->>'code' = 'chat_board_send_attachments_already_bound'
AND ${a}.payload->>'schemaVersion' = '1' AND length(${a}.payload->>'requestedByUserId') > 0
AND jsonb_typeof(${a}.payload->'attachmentIds') = 'array' AND jsonb_array_length(CASE WHEN jsonb_typeof(${a}.payload->'attachmentIds') = 'array' THEN ${a}.payload->'attachmentIds' ELSE '[]'::jsonb END) BETWEEN 1 AND 20
AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(${a}.payload->'attachmentIds') = 'array' THEN ${a}.payload->'attachmentIds' ELSE '[]'::jsonb END) rejected_id(value)
WHERE rejected_id.value !~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$')
AND EXISTS (SELECT 1 FROM chat_conversations conversation WHERE conversation.id = ${a}.conversation_id
AND conversation.company_id = ${a}.company_id AND conversation.endpoint_id = ${a}.endpoint_id
AND conversation.issue_id::text = ${a}.payload->>'issueId'
AND ${a}.provider_action_id = 'board-send-rejected:' || conversation.id::text || ':' || (${a}.payload->>'idempotencyKey'))
WHEN ${a}.kind = 'task_control_authorization' THEN ${a}.status IN ('processed', 'cancelled')
AND EXISTS (SELECT 1 FROM chat_publications p WHERE p.company_id = ${a}.company_id AND p.endpoint_id = ${a}.endpoint_id
AND p.conversation_id = ${a}.conversation_id AND ${a}.provider_action_id = 'task-control-authorization:' || p.id::text
AND ((p.state = 'published' AND p.published_at IS NOT NULL) OR p.state = 'cancelled'))
ELSE (${[completedGithubAction, completedSlackAction, completedTelegramAction]
ELSE (${[completedGithubAction, completedSlackAction, completedTelegramAction, completedTeamsAction, completedPhotonAction, completedDiscordAction, completedVoiceAction]
.map(proof => `(${proof(a)})`).join(' OR ')}) END`;
}
export const idleChatHistoryChecks = [
{ reason: "chat_deliveries", query: `WITH action_outcomes AS MATERIALIZED (
SELECT a.delivery_id, a.kind, (${completedAction('a')}) IS TRUE AS completed
FROM chat_actions a WHERE a.delivery_id IS NOT NULL
), action_receipts AS (
SELECT delivery_id, bool_and(completed) AS completed,
bool_or(kind = 'inbound_wakeup' AND completed) AS consumed
FROM action_outcomes GROUP BY delivery_id
) SELECT 1 FROM chat_deliveries d LEFT JOIN action_receipts receipt ON receipt.delivery_id = d.id WHERE (
{ reason: "chat_deliveries", query: `WITH candidate_deliveries AS MATERIALIZED (
SELECT d.id, d.company_id, d.endpoint_id, d.state, d.event_kind FROM chat_deliveries d WHERE (
EXISTS (SELECT 1 FROM chat_endpoints e WHERE e.id = d.endpoint_id AND e.company_id = d.company_id)
AND jsonb_typeof(d.normalized_event) = 'object'
AND d.normalized_event->>'providerEventId' = d.provider_event_id
AND d.normalized_event->>'kind' = d.event_kind
AND d.event_kind IN ('mention', 'message', 'direct_message', 'message_updated', 'message_deleted', 'message_restored',
'reaction_added', 'reaction_removed', 'action', 'modal_submitted', 'modal_closed', 'slash_command', 'file_shared', 'installation', 'uninstallation', 'unknown')
AND (d.state <> 'processed' OR d.event_kind NOT IN ('mention', 'message', 'direct_message')
OR receipt.consumed IS TRUE)
AND d.next_attempt_at IS NULL AND (d.state = 'failed' OR (d.state IN ('processed', 'filtered') AND d.processed_at IS NOT NULL))
AND (d.conversation_id IS NULL OR EXISTS (SELECT 1 FROM chat_conversations c WHERE c.id = d.conversation_id
AND c.company_id = d.company_id AND c.endpoint_id = d.endpoint_id))
AND COALESCE(receipt.completed, true)) IS NOT TRUE` },
) IS TRUE
), action_outcomes AS MATERIALIZED (
SELECT a.delivery_id, a.kind, (${completedAction('a')}) IS TRUE AS completed
FROM chat_actions a JOIN candidate_deliveries candidate ON candidate.id = a.delivery_id
), action_receipts AS MATERIALIZED (
SELECT delivery_id, bool_and(completed) AS completed,
bool_or(kind = 'inbound_wakeup' AND completed) AS consumed
FROM action_outcomes GROUP BY delivery_id
) SELECT 1 FROM chat_deliveries d WHERE (
EXISTS (SELECT 1 FROM chat_endpoints e WHERE e.id = d.endpoint_id AND e.company_id = d.company_id)
AND jsonb_typeof(d.normalized_event) = 'object'
AND d.normalized_event->>'providerEventId' = d.provider_event_id
AND d.normalized_event->>'kind' = d.event_kind
AND d.event_kind IN ('mention', 'message', 'direct_message', 'message_updated', 'message_deleted', 'message_restored',
'reaction_added', 'reaction_removed', 'action', 'modal_submitted', 'modal_closed', 'slash_command', 'file_shared', 'installation', 'uninstallation', 'unknown')
AND d.next_attempt_at IS NULL AND (d.state = 'failed' OR (d.state IN ('processed', 'filtered') AND d.processed_at IS NOT NULL))
AND (d.conversation_id IS NULL OR EXISTS (SELECT 1 FROM chat_conversations c WHERE c.id = d.conversation_id
AND c.company_id = d.company_id AND c.endpoint_id = d.endpoint_id))
) IS NOT TRUE
UNION ALL SELECT 1 FROM candidate_deliveries d
WHERE d.id IN (SELECT receipt.delivery_id FROM action_receipts receipt WHERE NOT receipt.completed)
OR (d.state = 'processed' AND d.event_kind IN ('mention', 'message', 'direct_message')
AND d.id NOT IN (SELECT receipt.delivery_id FROM action_receipts receipt WHERE receipt.consumed))` },
{ reason: "chat_publications", query: `SELECT 1 FROM chat_publications p WHERE (
(p.state = 'published' AND p.published_at IS NOT NULL AND p.redacted_error IS NULL
OR (p.state IN ('failed', 'cancelled') AND p.next_attempt_at IS NULL))
@@ -1,11 +1,10 @@
import { stagePhotonResponseNotice } from "./response-notices.js";
import { notifyChatReceiver } from "../runtime/receiver-work-notifications.js";
import type { EndpointRow, DbTransaction, RuntimeContext, LifecycleRuntimeFence } from "../../chat-channels/runtime-types.js";
import { notifyChatPublicationWork } from "../../chat-work-notifications.js";
import { nativeSha256 } from "../../native-runtime/canonical.js";
import { projectSafeChatPublicationText } from "../../chat-publication-projection.js";
import { PhotonAnswerValidationError, nativePhotonInteraction, photonResponseCommand, parsePhotonQuestionAnswer, type PhotonPromptReceipt, type PhotonInteractionBinding, type PhotonDraft } from "../../photon/interactions.js";
import { validateNativeQuestionResponseInput } from "../../native-runtime/native-question-bridge.js";
import type { AskUserQuestionsAnswer, AskUserQuestionsInteraction, IssueThreadInteraction } from "@paperclipai/shared";
@@ -368,41 +367,9 @@ export function createPhotonEvents(db: Db, options: PhotonEventsOptions) {
}
const enqueueResponse = async (key: string, text: string) => {
await assertCurrent();
await db.transaction(async (tx) => {
await tx
.insert(chatActions)
.values({
companyId: endpoint.companyId,
endpointId: endpoint.id,
conversationId: conversation.id,
kind: "photon_response_notice",
providerActionId: `photon-notice:${key}`,
payload: {
reference,
questionIndex,
publicationId: binding.publicationId,
},
status: "processed",
})
.onConflictDoNothing();
await notifyChatPublicationWork(tx);
await tx
.insert(chatPublications)
.values({
companyId: endpoint.companyId,
endpointId: endpoint.id,
conversationId: conversation.id,
issueId: conversation.issueId,
idempotencyKey: key,
payload: {
...publication.payload,
card: undefined,
text: projectSafeChatPublicationText(text),
},
state: "pending",
})
.onConflictDoNothing();
});
await db.transaction(tx => stagePhotonResponseNotice(tx, {
publication, reference, questionIndex, key, text,
}));
scheduleMessageProcessing(() =>
processPendingPublications().then(() => undefined),
);
@@ -0,0 +1,58 @@
/** Photon tokens and immutable provenance receipts do not own a turn once their
* exact canonical owner has settled. Receiver ownership remains a separate gate. */
export function completedPhotonAction(a: string): string {
const publication = (id: string) => `EXISTS (SELECT 1 FROM chat_publications publication
WHERE publication.id::text = ${id} AND publication.company_id = ${a}.company_id
AND publication.endpoint_id = ${a}.endpoint_id AND publication.conversation_id = ${a}.conversation_id
AND ((publication.state = 'published' AND publication.published_at IS NOT NULL)
OR (publication.state IN ('failed', 'cancelled') AND publication.next_attempt_at IS NULL)))`;
return `(${a}.kind = 'photon_interaction' AND (${a}.status IN ('issued', 'processed') OR (${a}.status = 'expired' AND ${a}.result->>'code' = 'interaction_resolved_elsewhere'))
AND ${a}.payload->>'version' = '1' AND length(${a}.payload->>'reference') > 0
AND ${a}.provider_action_id = 'photon:' || (${a}.payload->>'reference')
AND EXISTS (SELECT 1 FROM issue_thread_interactions interaction JOIN chat_publications publication
ON publication.id::text = ${a}.payload->>'publicationId' AND publication.company_id = interaction.company_id
AND publication.issue_id = interaction.issue_id AND publication.payload->>'interactionId' = interaction.id::text
JOIN chat_conversations conversation ON conversation.id = publication.conversation_id
AND conversation.company_id = interaction.company_id AND conversation.issue_id = interaction.issue_id
WHERE interaction.id::text = ${a}.payload->>'interactionId' AND interaction.company_id = ${a}.company_id
AND interaction.kind IN ('ask_user_questions', 'request_confirmation')
AND interaction.status IN ('answered', 'accepted', 'rejected', 'cancelled', 'expired') AND interaction.resolved_at IS NOT NULL
AND publication.endpoint_id = ${a}.endpoint_id AND conversation.id = ${a}.conversation_id
AND conversation.session_generation::text = ${a}.payload->>'sessionGeneration'))
OR (${a}.kind = 'photon_publication_retry' AND ${a}.status = 'processed'
AND length(${a}.result->>'authorizedByUserId') > 0 AND ${a}.payload->>'attempt' ~ '^[1-9][0-9]*$'
AND ${a}.provider_action_id = 'photon-retry:' || (${a}.payload->>'publicationId') || ':' || (${a}.payload->>'attempt')
AND ${publication(`${a}.payload->>'publicationId'`)})
OR (${a}.kind = 'photon_response_notice' AND ${a}.status = 'processed' AND ${a}.result IS NULL
AND length(${a}.payload->>'reference') > 0 AND ${a}.payload->>'questionIndex' ~ '^[0-9]+$'
AND EXISTS (SELECT 1 FROM chat_publications notice WHERE notice.company_id = ${a}.company_id
AND notice.endpoint_id = ${a}.endpoint_id AND notice.conversation_id = ${a}.conversation_id
AND ${a}.provider_action_id = 'photon-notice:' || notice.idempotency_key
AND ((notice.state = 'published' AND notice.published_at IS NOT NULL)
OR (notice.state IN ('failed', 'cancelled') AND notice.next_attempt_at IS NULL)))
AND EXISTS (SELECT 1 FROM chat_publications source WHERE source.id::text = ${a}.payload->>'publicationId'
AND source.company_id = ${a}.company_id AND source.endpoint_id = ${a}.endpoint_id
AND source.conversation_id = ${a}.conversation_id))
OR (${a}.kind IN ('attachment_derivative', 'attachment_companion') AND ${a}.status = 'processed' AND ${a}.result IS NULL
AND EXISTS (SELECT 1 FROM chat_endpoints endpoint WHERE endpoint.id = ${a}.endpoint_id
AND endpoint.company_id = ${a}.company_id AND endpoint.provider = 'imessage-photon')
AND ${a}.delivery_id IS NOT NULL
AND ((${a}.kind = 'attachment_derivative' AND ${a}.payload->>'kind' = 'heif_jpeg_preview')
OR (${a}.kind = 'attachment_companion' AND ${a}.payload->>'kind' = 'live_photo_video'))
AND ${a}.payload->>'originalAttachmentId' ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'
AND ${a}.payload->>'derivativeAttachmentId' ~ '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$'
AND ${a}.payload->>'originalAttachmentId' <> ${a}.payload->>'derivativeAttachmentId'
AND ${a}.payload->>'originalSha256' ~ '^[a-f0-9]{64}$' AND ${a}.payload->>'derivativeSha256' ~ '^[a-f0-9]{64}$'
AND ${a}.provider_action_id = 'photon-' || (${a}.payload->>'kind') || ':' || ${a}.delivery_id::text || ':' || (${a}.payload->>'originalAttachmentId')
AND NOT EXISTS (SELECT 1 FROM issue_attachments attachment WHERE attachment.id::text IN
(${a}.payload->>'originalAttachmentId', ${a}.payload->>'derivativeAttachmentId')
AND (attachment.company_id <> ${a}.company_id OR NOT EXISTS (SELECT 1 FROM assets asset
WHERE asset.id = attachment.asset_id AND asset.company_id = ${a}.company_id AND asset.sha256 = CASE
WHEN attachment.id::text = ${a}.payload->>'originalAttachmentId' THEN ${a}.payload->>'originalSha256'
ELSE ${a}.payload->>'derivativeSha256' END)))
AND NOT EXISTS (SELECT 1 FROM issue_attachments original JOIN issue_attachments derivative
ON derivative.id::text = ${a}.payload->>'derivativeAttachmentId'
WHERE original.id::text = ${a}.payload->>'originalAttachmentId'
AND (original.company_id <> derivative.company_id OR original.issue_id <> derivative.issue_id
OR original.issue_comment_id IS DISTINCT FROM derivative.issue_comment_id)))`;
}
@@ -0,0 +1,27 @@
import { chatActions, chatPublications, type Db } from "@paperclipai/db";
import { notifyChatPublicationWork } from "../../chat-work-notifications.js";
import { projectSafeChatPublicationText } from "../../chat-publication-projection.js";
type Tx = Parameters<Parameters<Db["transaction"]>[0]>[0];
/** Keep the notice marker and its outbox row in the same owner transaction. */
export async function stagePhotonResponseNotice(tx: Tx, input: {
publication: typeof chatPublications.$inferSelect;
reference: string;
questionIndex: number;
key: string;
text: string;
}) {
const { publication, reference, questionIndex, key, text } = input;
await tx.insert(chatActions).values({
companyId: publication.companyId, endpointId: publication.endpointId, conversationId: publication.conversationId,
kind: "photon_response_notice", providerActionId: `photon-notice:${key}`,
payload: { reference, questionIndex, publicationId: publication.id }, status: "processed",
}).onConflictDoNothing();
await notifyChatPublicationWork(tx);
await tx.insert(chatPublications).values({
companyId: publication.companyId, endpointId: publication.endpointId, conversationId: publication.conversationId,
issueId: publication.issueId, idempotencyKey: key,
payload: { ...publication.payload, card: undefined, text: projectSafeChatPublicationText(text) }, state: "pending",
}).onConflictDoNothing();
}
@@ -0,0 +1,35 @@
/** Teams keeps immutable provider-effect and recipient-intent receipts after
* delivery. Only the exact closed transfer/publication retires that authority. */
export function completedTeamsAction(a: string): string {
return `${a}.status = 'processed' AND ${a}.kind IN ('teams_file_effect_intent', 'teams_file_consent', 'teams_board_file_intent')
AND EXISTS (SELECT 1 FROM chat_teams_file_transfers transfer JOIN chat_publications publication
ON publication.id = transfer.publication_id AND publication.company_id = transfer.company_id
WHERE transfer.company_id = ${a}.company_id AND transfer.endpoint_id = ${a}.endpoint_id
AND transfer.conversation_id = ${a}.conversation_id
AND publication.id::text = ${a}.payload->>'publicationId'
AND publication.endpoint_id = ${a}.endpoint_id AND publication.conversation_id = ${a}.conversation_id
AND transfer.phase IN ('delivered', 'declined', 'expired', 'cancelled')
AND transfer.attempt_id IS NULL AND transfer.attempt_expires_at IS NULL
AND ((publication.state = 'published' AND publication.published_at IS NOT NULL)
OR (publication.state IN ('failed', 'cancelled') AND publication.next_attempt_at IS NULL))
AND CASE WHEN ${a}.kind = 'teams_file_effect_intent' THEN
${a}.payload->>'schema' = 'paperclip.teams.file-effect-intent.v1'
AND ${a}.payload->>'transferId' = transfer.id::text
AND ${a}.provider_action_id = 'teams-file-effect:' || (${a}.payload->>'attemptId')
AND ${a}.payload->>'stage' IN ('consent', 'upload', 'file_info')
WHEN ${a}.kind = 'teams_file_consent' THEN
${a}.payload->>'schema' = 'paperclip.teams.consent-receipt.v1'
AND ${a}.payload->>'transferId' = transfer.id::text
AND ${a}.payload->>'receiptDigest' = transfer.response_digest
AND ${a}.payload->>'action' IN ('accept', 'decline')
AND ${a}.principal_id IS NOT DISTINCT FROM transfer.principal_id
ELSE ${a}.payload->>'schema' = 'paperclip.teams.board-file-intent.v1'
AND ${a}.provider_action_id = 'teams_board_file_intent:' || publication.id::text
AND ${a}.payload->>'issueId' = transfer.issue_id::text
AND ${a}.payload->>'attachmentId' = transfer.attachment_id::text
AND ${a}.payload->>'commentId' = publication.comment_id::text
AND ${a}.payload->>'sourceScopeSha256' ~ '^[a-f0-9]{64}$'
AND ${a}.payload->>'recipientBindingSha256' ~ '^[a-f0-9]{64}$'
AND ${a}.payload->>'byteIdentitySha256' ~ '^[a-f0-9]{64}$'
AND ${a}.payload->>'commentBodySha256' ~ '^[a-f0-9]{64}$' END)`;
}
@@ -1,3 +1,63 @@
/** Connector rows below are immutable authority/confirmed-call receipts. Their
* own downstream publication, interaction and run rows keep independent gates. */
function completedSlackConnectorAction(a: string): string {
const principal = `EXISTS (SELECT 1 FROM chat_external_principals principal WHERE principal.id = ${a}.principal_id
AND principal.company_id = ${a}.company_id AND principal.provider = 'slack')`;
const issue = `EXISTS (SELECT 1 FROM issues issue WHERE issue.id::text = ${a}.payload->>'issueId'
AND issue.company_id = ${a}.company_id AND (${a}.conversation_id IS NULL OR EXISTS (
SELECT 1 FROM chat_conversations conversation WHERE conversation.id = ${a}.conversation_id
AND conversation.company_id = issue.company_id AND conversation.issue_id = issue.id
AND conversation.endpoint_id = ${a}.endpoint_id)))`;
return `(${a}.kind = 'slack_private_source' AND ${a}.status = 'processed' AND ${a}.result IS NULL
AND ${issue} AND ${principal} AND length(${a}.payload->>'channelId') > 0
AND length(${a}.payload->>'requesterId') > 0
AND ${a}.provider_action_id = 'slack-private:' || (${a}.payload->>'issueId') || ':'
|| (${a}.payload->>'channelId') || ':' || (${a}.payload->>'requesterId'))
OR (${a}.kind = 'slack_connect' AND ${a}.status = 'processed'
AND ${a}.payload->>'version' = '1' AND ${a}.result->>'code' = 'slack_identity_discovered'
AND length(${a}.payload->>'channelId') > 0 AND ${principal}
AND EXISTS (SELECT 1 FROM chat_external_principals principal WHERE principal.id = ${a}.principal_id
AND principal.external_id = ${a}.payload->>'userId'))
OR (${a}.kind IN ('slack_test_message', 'slack_test_message_invitation') AND ${a}.status = 'processed'
AND length(${a}.payload->>'userId') > 0 AND ${a}.payload->>'slackUserId' ~ '^[UW][A-Z0-9]+$'
AND CASE WHEN ${a}.kind = 'slack_test_message' THEN ${a}.result->>'slackUrl' ~ '^https://app[.]slack[.]com/client/[^/]+/D[A-Z0-9]+$'
ELSE ${principal} AND ${a}.result->>'expiresAt' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T' END)
OR (${a}.kind = 'slack_managed_reconnect' AND ${a}.status = 'processed' AND ${a}.result IS NULL
AND ${a}.provider_action_id LIKE 'slack_managed_reconnect:%'
AND length(${a}.payload->>'registrationRequestId') > 0
AND EXISTS (SELECT 1 FROM chat_slack_manager_grants grant_record WHERE grant_record.id::text = ${a}.payload->>'grantId'
AND grant_record.company_id = ${a}.company_id))
OR (${a}.kind = 'slack_tool_write' AND ${a}.status = 'processed' AND jsonb_typeof(${a}.result) = 'object'
AND ${a}.payload->>'name' IN ('slack_open_dm', 'slack_post_message', 'slack_update_message', 'slack_delete_message',
'slack_add_reaction', 'slack_remove_reaction', 'slack_add_pin', 'slack_remove_pin', 'slack_add_bookmark',
'slack_remove_bookmark', 'slack_create_canvas', 'slack_edit_canvas', 'slack_replace_canvas_section',
'slack_share_list', 'slack_edit_list', 'slack_create_list', 'slack_create_list_item', 'slack_update_list_item',
'slack_invite', 'slack_create_channel', 'slack_upload_file')
AND ${a}.payload->>'hash' ~ '^[a-f0-9]{64}$' AND jsonb_typeof(${a}.payload->'args') = 'object'
AND length(${a}.payload->>'userId') > 0 AND length(${a}.payload->>'revision') > 0
AND ${a}.provider_action_id = 'slack-tool:' || (${a}.payload->'binding'->>'issueId') || ':' || (${a}.payload->'args'->>'idempotencyKey')
AND EXISTS (SELECT 1 FROM tool_invocations invocation JOIN heartbeat_runs run ON run.id = invocation.run_id
AND run.company_id = invocation.company_id AND run.agent_id = invocation.agent_id
WHERE invocation.id::text = ${a}.payload->>'invocationId' AND invocation.company_id = ${a}.company_id
AND invocation.run_id::text = ${a}.payload->'binding'->>'runId'
AND invocation.agent_id::text = ${a}.payload->'binding'->>'agentId'
AND invocation.issue_id::text = ${a}.payload->'binding'->>'issueId'
AND ${a}.payload->'binding'->>'companyId' = ${a}.company_id::text))
OR (${a}.kind = 'slack_board_message' AND ${issue} AND EXISTS (
SELECT 1 FROM issue_comments comment JOIN chat_publications publication
ON publication.comment_id = comment.id AND publication.company_id = comment.company_id
WHERE comment.id::text = ${a}.payload->>'commentId' AND comment.company_id = ${a}.company_id
AND comment.issue_id::text = ${a}.payload->>'issueId' AND comment.author_user_id = ${a}.payload->>'userId'
AND publication.endpoint_id = ${a}.endpoint_id AND publication.conversation_id = ${a}.conversation_id
AND ((publication.state = 'published' AND publication.published_at IS NOT NULL)
OR (publication.state IN ('failed', 'cancelled') AND publication.next_attempt_at IS NULL)))
AND ((${a}.status = 'cancelled' AND ${a}.result->>'code' = 'slack_board_work_not_authorized')
OR (${a}.status = 'processed' AND (${a}.result IS NULL OR (${a}.result->>'resumeApplied' = 'true'
AND EXISTS (SELECT 1 FROM agent_wakeup_requests wake WHERE wake.company_id = ${a}.company_id
AND wake.idempotency_key = 'slack-board-comment:' || ${a}.id::text AND wake.finished_at IS NOT NULL
AND wake.status IN ('completed', 'failed', 'cancelled', 'skipped', 'timed_out')))))))`;
}
export function completedSlackAction(a: string): string {
return `(${a}.kind = 'slack_session_sync' AND (
(${a}.status = 'processed' AND ${a}.result->>'outcome' IN ('updated', 'unsupported')
@@ -12,5 +72,6 @@ export function completedSlackAction(a: string): string {
AND ${a}.result->>'code' IN ('slack_session_stop_cancelled', 'slack_session_stop_target_superseded', 'slack_session_stop_no_current_work')
AND NOT EXISTS (SELECT 1 FROM heartbeat_runs r WHERE r.id::text = ${a}.result->>'runId'
AND (r.company_id <> ${a}.company_id OR r.status NOT IN ('succeeded', 'failed', 'cancelled', 'timed_out', 'interrupted')
OR r.finished_at IS NULL OR r.cost_accounting_pending)))`;
OR r.finished_at IS NULL OR r.cost_accounting_pending)))
OR (${completedSlackConnectorAction(a)})`;
}
@@ -1,5 +1,13 @@
export function completedTelegramAction(a: string): string {
return `(${a}.kind = 'telegram_maintenance' AND (
return `(${a}.kind = 'telegram_stop_subscription' AND ${a}.status = 'processed'
AND ${a}.result->>'code' = 'telegram_stop_subscription_confirmed' AND ${a}.payload->>'version' = '1'
AND ${a}.payload->>'botUserId' ~ '^[1-9][0-9]{0,15}$'
AND ${a}.payload->>'credentialFingerprint' ~ '^[a-f0-9]{64}$'
AND ${a}.payload->>'webhookUrlSha256' ~ '^[a-f0-9]{64}$'
AND ${a}.payload->>'runtimeGeneration' ~ '^[0-9]+$'
AND ${a}.provider_action_id = 'telegram-stop-subscription:' || (${a}.payload->>'runtimeGeneration') || ':'
|| (${a}.payload->>'credentialFingerprint') || ':' || (${a}.payload->>'webhookUrlSha256'))
OR (${a}.kind = 'telegram_maintenance' AND (
(${a}.status = 'processed' AND (${a}.result->>'providerConfirmed' = 'true'
OR ${a}.result->>'code' = 'telegram_stop_subscription_confirmed'))
OR (${a}.status = 'cancelled' AND ${a}.result->>'code' = 'telegram_maintenance_credentials_superseded')
@@ -0,0 +1,54 @@
/** Form-open tokens stay issued: resolving the canonical question revokes their
* authority. A processed submission additionally needs its exact answer receipt. */
export function completedQuestionFormAction(a: string): string {
const resolved = `EXISTS (SELECT 1 FROM issue_thread_interactions interaction
JOIN chat_conversations conversation ON conversation.issue_id = interaction.issue_id AND conversation.company_id = interaction.company_id
JOIN chat_publications publication ON publication.id::text = ${a}.payload->>'publicationId'
AND publication.company_id = interaction.company_id AND publication.issue_id = interaction.issue_id
AND publication.conversation_id = conversation.id AND publication.endpoint_id = conversation.endpoint_id
WHERE interaction.id::text = ${a}.payload->>'interactionId' AND interaction.company_id = ${a}.company_id
AND (interaction.kind = 'ask_user_questions' OR (interaction.kind = 'request_item_verdicts' AND EXISTS (
SELECT 1 FROM chat_endpoints endpoint WHERE endpoint.id = ${a}.endpoint_id AND endpoint.company_id = ${a}.company_id AND endpoint.provider = 'slack'))) AND interaction.status IN ('answered', 'cancelled', 'expired')
AND interaction.resolved_at IS NOT NULL AND conversation.id = ${a}.conversation_id
AND conversation.endpoint_id = ${a}.endpoint_id AND publication.payload->>'interactionId' = interaction.id::text)`;
return `(${a}.kind IN ('question_form_open', 'question_form_submit') AND ${a}.payload->>'version' = '1'
AND ${resolved} AND (
(${a}.kind = 'question_form_open' AND (${a}.status = 'issued' OR (${a}.status = 'expired'
AND ${a}.result->>'code' IN ('question_form_submitted', 'interaction_resolved_elsewhere'))) AND EXISTS (
SELECT 1 FROM chat_actions submit WHERE submit.company_id = ${a}.company_id AND submit.endpoint_id = ${a}.endpoint_id
AND submit.conversation_id = ${a}.conversation_id AND submit.kind = 'question_form_submit'
AND submit.provider_action_id = ${a}.payload->>'formActionId'
AND submit.payload->>'formActionId' = submit.provider_action_id
AND submit.payload->>'interactionId' = ${a}.payload->>'interactionId'
AND submit.payload->>'publicationId' = ${a}.payload->>'publicationId'
AND submit.payload->>'expiresAt' = ${a}.payload->>'expiresAt'
AND (submit.status = 'issued' OR (submit.status = 'expired' AND submit.result->>'code' = 'interaction_resolved_elsewhere') OR (submit.status = 'processed'
AND submit.result->>'code' = 'question_form_answered'
AND submit.result->>'interactionId' = submit.payload->>'interactionId'))))
OR (${a}.kind = 'question_form_submit' AND ${a}.payload->>'formActionId' = ${a}.provider_action_id
AND (${a}.status = 'issued' OR (${a}.status = 'expired' AND ${a}.result->>'code' = 'interaction_resolved_elsewhere') OR (${a}.status = 'processed' AND ${a}.result->>'code' = 'question_form_answered'
AND ${a}.result->>'interactionId' = ${a}.payload->>'interactionId')))))
OR (${a}.kind = 'question_answer' AND ${resolved}
AND (${a}.status = 'issued' OR (${a}.status = 'processed'
AND ${a}.result->>'interactionId' = ${a}.payload->>'interactionId')
OR (${a}.status = 'expired' AND ${a}.result->>'code' IN ('interaction_resolved_by_sibling_action', 'interaction_resolved_elsewhere'))))
OR (${a}.kind = 'question_form_open_attempt' AND ${a}.status = 'processed'
AND ${a}.result->>'code' = 'question_form_opened')`;
}
/** The terminal interaction writer expires its unused callback tokens after a
* board/sibling resolution. Expiry alone never proves canonical completion. */
export function completedExpiredChatControl(a: string): string {
return `${a}.kind = 'confirmation_response' AND ${a}.status = 'expired'
AND ${a}.result->>'code' = 'interaction_resolved_elsewhere' AND ${a}.payload->>'version' = '1'
AND ${a}.payload->>'decision' IN ('accept', 'reject') AND EXISTS (
SELECT 1 FROM issue_thread_interactions interaction JOIN chat_publications publication
ON publication.id::text = ${a}.payload->>'publicationId' AND publication.company_id = interaction.company_id
AND publication.issue_id = interaction.issue_id AND publication.payload->>'interactionId' = interaction.id::text
JOIN chat_conversations conversation ON conversation.id = publication.conversation_id
AND conversation.company_id = interaction.company_id AND conversation.issue_id = interaction.issue_id
WHERE interaction.id::text = ${a}.payload->>'interactionId' AND interaction.company_id = ${a}.company_id
AND interaction.kind IN ('request_confirmation', 'request_checkbox_confirmation', 'suggest_tasks', 'connection_intent')
AND interaction.status IN ('accepted', 'rejected', 'cancelled', 'expired') AND interaction.resolved_at IS NOT NULL
AND publication.endpoint_id = ${a}.endpoint_id AND conversation.id = ${a}.conversation_id)`;
}
+1 -1
View File
@@ -17,7 +17,7 @@ import { idleBrowserHistoryChecks } from "./browser-use-idle-proof.js";
import { idleDecisionHistoryChecks } from "./decision-idle-proof.js";
import { idleWorkspaceHistoryChecks } from "./workspace-history-idle-proof.js";
import { idleMcpHistoryChecks } from "./public-mcp/idle-history-proof.js";
import { idleExternalAgentHistoryChecks } from "../modules/external-agents/idle-history-proof.js";
import { idleExternalAgentHistoryChecks } from "../modules/external-agents/index.js";
export const completedHistoryChecks = [
...idleChatHistoryChecks, ...idleToolActionHistoryChecks, ...idleBrowserHistoryChecks,
+7 -1
View File
@@ -1,3 +1,4 @@
import { recordMcpAttachmentCleanup, type McpAttachmentCleanup } from "./public-mcp/upload-cleanup.js";
import { notifyStrandedAgentStatus, notifyActiveRunSilence } from "./live-run-recovery-notifications.js";
import { finishedIdleWake } from "./idle-work-proof.js";
import { notifyDeferredExecutionEligibility } from "./deferred-execution-notifications.js";
@@ -13907,7 +13908,7 @@ export function issueService(db: Db) {
.where(eq(issueAttachments.id, id))
.then((rows) => rows[0] ?? null),
removeAttachment: async (id: string) =>
removeAttachment: async (id: string, options?: { mcpCleanup?: McpAttachmentCleanup | null }) =>
db.transaction(async (tx) => {
const existing = await tx
.select({
@@ -13937,6 +13938,11 @@ export function issueService(db: Db) {
await notifyIssueRecovery(tx, { companyId: existing.companyId });
await tx.delete(issueAttachments).where(eq(issueAttachments.id, id));
await tx.delete(assets).where(eq(assets.id, existing.assetId));
if (options?.mcpCleanup?.id === existing.id && options.mcpCleanup.companyId === existing.companyId
&& options.mcpCleanup.issueId === existing.issueId && options.mcpCleanup.provider === existing.provider
&& options.mcpCleanup.objectKey === existing.objectKey) {
await recordMcpAttachmentCleanup(tx, options.mcpCleanup);
}
return existing;
}),
@@ -9897,6 +9897,11 @@ describe("native warm session supervision", () => {
it("blocks idle sleep for a released local warm session until its close checkpoint finishes", async () => {
await closeIdleWarmNativeSessionsForRestart();
const { reconcilePersistedRuntimeServicesOnStartup, readWorkspaceRuntimeIdleWork } = await import("../workspace-runtime.js");
// The real server reconstructs the workspace owner before startup can
// become quiet. This fixture has no runtime rows or desired services.
await reconcilePersistedRuntimeServicesOnStartup(leaseDb());
expect(readWorkspaceRuntimeIdleWork()).toBe("none");
markIdleIngressTracked(); markIdleStartupComplete({ scheduledBackups: false });
let finishClose!: () => void;
const checkpoint = new Promise<void>(resolve => { finishClose = resolve; });
@@ -80,13 +80,22 @@ export function createPublicMcpTransfers(db: Db, oauth: PublicMcpOAuth, api: Api
}
async function sweep() {
await db.delete(mcpFileTickets).where(lt(mcpFileTickets.expiresAt, new Date()));
// Serialize cleanup against completion/reissue. Never remove a committed file.
// Serialize cleanup against completion/reissue. Requalify old optimistic
// cleanup and orphaned removed attachments, never a still-linked file.
await db.transaction(async tx => {
const abandoned = await tx.select().from(mcpAttachmentUploads).where(and(isNull(mcpAttachmentUploads.attachmentId), isNull(mcpAttachmentUploads.cleanedAt), lt(mcpAttachmentUploads.expiresAt, new Date()))).limit(20).for("update", { skipLocked: true });
const abandoned = await tx.select().from(mcpAttachmentUploads).where(and(
isNull(mcpAttachmentUploads.cleanupVerifiedAt), lt(mcpAttachmentUploads.expiresAt, new Date()),
sql`(${mcpAttachmentUploads.attachmentId} IS NULL OR NOT EXISTS (
SELECT 1 FROM issue_attachments attachment WHERE attachment.id = ${mcpAttachmentUploads.attachmentId}))`,
)).limit(20).for("update", { skipLocked: true });
for (const upload of abandoned) {
if (upload.storageProvider !== storage.provider) continue;
await storage.deleteObject(upload.companyId, upload.objectKey);
await tx.update(mcpAttachmentUploads).set({ cleanedAt: new Date() }).where(eq(mcpAttachmentUploads.id, upload.id));
if ((await storage.headObject(upload.companyId, upload.objectKey)).exists !== false) {
throw new Error("MCP upload storage cleanup could not be confirmed");
}
const verifiedAt = new Date();
await tx.update(mcpAttachmentUploads).set({ cleanedAt: verifiedAt, cleanupVerifiedAt: verifiedAt }).where(eq(mcpAttachmentUploads.id, upload.id));
}
});
}
@@ -133,6 +142,7 @@ export function createPublicMcpTransfers(db: Db, oauth: PublicMcpOAuth, api: Api
const published = await db.transaction(async tx => {
const [upload] = await tx.select().from(mcpAttachmentUploads).where(eq(mcpAttachmentUploads.id, row.uploadId!)).for("update");
if (!upload || upload.companyId !== principal.grant.companyId || upload.userId !== principal.grant.userId || upload.workSessionId !== row.workSessionId || upload.grantId && upload.grantId !== row.grantId || upload.storageProvider !== storage.provider) throw unavailable();
if (upload.expiresAt <= new Date() || upload.cleanedAt || upload.cleanupVerifiedAt) throw unavailable();
if (upload.sha256 !== digest || upload.byteSize !== bytes.length || req.get("Content-Type")?.split(";")[0]?.trim().toLowerCase() !== upload.contentType) throw new McpCapabilityError("Upload bytes, size or Content-Type do not match the requested file.");
await taskAccess(principal, upload.taskId, principal.grant.purpose !== "agent", tx as unknown as Db, row.workSessionId ?? undefined);
const stored = upload.attachmentId ? null : await storage.putFile({ companyId: upload.companyId, namespace: "mcp-transfers", objectKey: upload.objectKey, originalFilename: upload.originalFilename, contentType: upload.contentType, body: bytes });
@@ -200,7 +210,11 @@ export function createPublicMcpTransfers(db: Db, oauth: PublicMcpOAuth, api: Api
objectKey: `${p.grant.companyId}/mcp-transfers/${id}`, storageProvider: storage.provider, expiresAt: new Date(Date.now() + lifetime) }).returning();
}
if (!found || found.argumentsHash !== hash) throw new McpCapabilityError("requestId was already used for a different upload.");
await tx.update(mcpAttachmentUploads).set({ expiresAt: new Date(Date.now() + lifetime), cleanedAt: null }).where(eq(mcpAttachmentUploads.id, found.id));
// A committed identity is immutable, including after user deletion.
// A rejected replay must not erase verified cleanup or permit new bytes.
if (!found.attachmentId) await tx.update(mcpAttachmentUploads)
.set({ expiresAt: new Date(Date.now() + lifetime), cleanedAt: null, cleanupVerifiedAt: null })
.where(eq(mcpAttachmentUploads.id, found.id));
return found;
});
if (upload.attachmentId) {
@@ -22,14 +22,17 @@ export const idleMcpHistoryChecks = [
OR (d.mailbox_item_id IS NOT NULL AND d.activity_id IS NULL AND EXISTS (
SELECT 1 FROM dot_mailbox_items m WHERE m.id = d.mailbox_item_id
AND m.company_id = s.company_id AND m.binding_id = s.binding_id))))) IS NOT TRUE` },
// The upload owner allocates object identity before storage writes and only
// stamps cleaned_at after deletion. A committed attachment is never deleted.
// Upload identity survives user deletion. Only the new owner-verified
// absence stamp can retire cleanup; legacy cleaned_at alone was optimistic.
{ reason: "mcp_attachment_uploads", query: `SELECT 1 FROM mcp_attachment_uploads u WHERE (
EXISTS (SELECT 1 FROM issues i WHERE i.id = u.task_id AND i.company_id = u.company_id)
EXISTS (SELECT 1 FROM companies c WHERE c.id = u.company_id)
AND NOT EXISTS (SELECT 1 FROM issues contradiction WHERE contradiction.id = u.task_id AND contradiction.company_id <> u.company_id)
AND (u.grant_id IS NULL OR EXISTS (SELECT 1 FROM mcp_oauth_grants g WHERE g.id = u.grant_id
AND g.company_id = u.company_id AND g.user_id = u.user_id))
AND ((u.attachment_id IS NULL AND u.cleaned_at IS NOT NULL)
OR (u.cleaned_at IS NULL AND u.attachment_id IS NOT NULL AND EXISTS (
AND ((u.cleaned_at IS NOT NULL AND u.cleanup_verified_at IS NOT NULL AND u.cleanup_verified_at >= u.cleaned_at
AND (u.attachment_id IS NULL OR NOT EXISTS (SELECT 1 FROM issue_attachments live WHERE live.id = u.attachment_id)))
OR (u.cleaned_at IS NULL AND u.cleanup_verified_at IS NULL AND u.attachment_id IS NOT NULL
AND EXISTS (SELECT 1 FROM issues i WHERE i.id = u.task_id AND i.company_id = u.company_id) AND EXISTS (
SELECT 1 FROM issue_attachments a JOIN assets stored ON stored.id = a.asset_id
WHERE a.id = u.attachment_id AND a.company_id = u.company_id AND a.issue_id = u.task_id
AND stored.company_id = u.company_id AND stored.object_key = u.object_key
@@ -0,0 +1,38 @@
import { and, eq } from "drizzle-orm";
import { mcpAttachmentUploads, type Db } from "@paperclipai/db";
import type { StorageService } from "../../storage/types.js";
type AttachmentStorage = {
id: string;
companyId: string;
issueId: string;
provider: string;
objectKey: string;
};
export type McpAttachmentCleanup = AttachmentStorage & { verifiedAt: Date };
function uploadIdentity(attachment: AttachmentStorage) {
return and(eq(mcpAttachmentUploads.companyId, attachment.companyId),
eq(mcpAttachmentUploads.taskId, attachment.issueId), eq(mcpAttachmentUploads.attachmentId, attachment.id),
eq(mcpAttachmentUploads.storageProvider, attachment.provider), eq(mcpAttachmentUploads.objectKey, attachment.objectKey));
}
/** Called after deletion by the attachment owner. A resolved delete alone can
* be optimistic; only the exact upload's storage absence authorizes cleanup. */
export async function confirmMcpAttachmentCleanup(db: Db, storage: StorageService, attachment: AttachmentStorage): Promise<McpAttachmentCleanup | null> {
const [upload] = await db.select({ id: mcpAttachmentUploads.id }).from(mcpAttachmentUploads).where(uploadIdentity(attachment)).limit(1);
if (!upload) return null;
if (storage.provider !== attachment.provider || (await storage.headObject(attachment.companyId, attachment.objectKey)).exists !== false) {
throw new Error("MCP attachment storage cleanup could not be confirmed");
}
return { ...attachment, verifiedAt: new Date() };
}
/** Keep attachmentId as immutable replay identity. For attachment removal this
* runs in the metadata deletion transaction; issue removal stamps each object
* after its existing post-transaction storage cleanup completes. */
export async function recordMcpAttachmentCleanup(db: Pick<Db, "update">, cleanup: McpAttachmentCleanup | null | undefined) {
if (!cleanup) return;
await db.update(mcpAttachmentUploads).set({ cleanedAt: cleanup.verifiedAt, cleanupVerifiedAt: cleanup.verifiedAt }).where(uploadIdentity(cleanup));
}
@@ -0,0 +1,71 @@
import { VOICE_CALL_REPORT_WINDOW_MS } from "./voice-call-reports.js";
/** Available reports still own refresh work for the owner's whole 24h window.
* The accepted push receipt proves submission, never unobservable playback. */
export function completedVoiceAction(a: string): string {
const closedSession = (s: string) => `${s}.state IN ('ended', 'failed', 'expired') AND ${s}.ended_at IS NOT NULL
AND COALESCE(${s}.error_code, '') <> 'cleanup_pending'
AND (${s}.provider_session_id IS NULL OR ${s}.ended_at < now() - interval '${VOICE_CALL_REPORT_WINDOW_MS} milliseconds')`;
const submitted = (alias: string) => `(${alias}.status = 'completed' AND length(${alias}.result->>'messageId') > 0
AND ${alias}.result->>'playback' = 'unknown' AND ${alias}.result->>'acceptedAt' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}T')
OR (${alias}.status = 'call_ended' AND ${alias}.result->>'errorCode' = 'provider_unavailable')
OR (${alias}.status = 'failed' AND ${alias}.result->>'errorCode' IN
('credentials_rejected', 'insufficient_credits', 'provider_unavailable', 'voice_authority_unavailable'))`;
// Nonnull company/endpoint keys let PostgreSQL hash each debt set once,
// rather than rescanning every old session for every retained voice marker.
const settled = `(${a}.company_id, ${a}.endpoint_id) NOT IN (
SELECT session.company_id, session.endpoint_id FROM chat_voice_sessions session
WHERE (${closedSession('session')}) IS NOT TRUE)
AND (${a}.company_id, ${a}.endpoint_id) NOT IN (
SELECT admission.company_id, admission.endpoint_id FROM chat_voice_inbound_calls admission
WHERE admission.state NOT IN ('denied', 'expired', 'ended')
OR admission.cleanup_confirmed_at IS NULL OR admission.cleanup_next_attempt_at IS NOT NULL)
AND (${a}.company_id, ${a}.endpoint_id) NOT IN (
SELECT push.company_id, push.endpoint_id FROM chat_actions push
WHERE push.kind = 'speko_voice_reply_push' AND (${submitted('push')}) IS NOT TRUE)`;
// Configuration records this existing audit event only after tools, webhook,
// voice defaults and runtime setup all succeeded. A partial setup cannot use
// individual resource receipts to certify the rest of the owning operation.
const setupCommitted = `EXISTS (SELECT 1 FROM activity_log receipt JOIN chat_endpoints endpoint
ON endpoint.id = ${a}.endpoint_id AND endpoint.company_id = ${a}.company_id
WHERE receipt.company_id = ${a}.company_id AND receipt.entity_type = 'tool_connection'
AND receipt.entity_id = endpoint.connection_id::text AND receipt.details->>'endpointId' = endpoint.id::text
AND receipt.details->>'provider' = 'speko' AND receipt.action IN ('chat_endpoint.credentials_verified', 'chat_endpoint.reconnected')
AND receipt.created_at >= ${a}.updated_at)`;
return `EXISTS (SELECT 1 FROM chat_endpoints endpoint WHERE endpoint.id = ${a}.endpoint_id
AND endpoint.company_id = ${a}.company_id AND endpoint.provider = 'speko') AND ${settled} AND (
(${a}.kind = 'speko_tool_setup' AND ${a}.status = 'completed' AND ${setupCommitted}
AND ${a}.payload->>'name' IN ('submit_request', 'get_updates', 'answer_question')
AND ${a}.payload->>'fingerprint' ~ '^[a-f0-9]{64}$' AND ${a}.payload->>'ownershipDefinitionHash' ~ '^[a-f0-9]{64}$'
AND length(${a}.payload->>'ownershipAgentId') > 0 AND length(${a}.result->>'toolId') > 0
AND ${a}.provider_action_id = 'speko_tool:' || (${a}.payload->>'name') || ':' || (${a}.payload->>'fingerprint'))
OR (${a}.kind = 'speko_lifecycle_setup' AND ${a}.status = 'completed' AND ${setupCommitted}
AND ${a}.payload->>'fingerprint' ~ '^[a-f0-9]{64}$' AND length(${a}.payload->>'ownershipAgentId') > 0
AND ${a}.payload->>'ownershipUrl' LIKE 'https://%/events' AND length(${a}.result->>'webhookId') > 0
AND ${a}.provider_action_id = 'speko_lifecycle:' || (${a}.payload->>'fingerprint'))
OR (${a}.kind = 'speko_lifecycle_receipt' AND ${a}.status = 'completed' AND ${a}.result->>'accepted' = 'true'
AND ${a}.payload->>'type' IN ('call.status', 'call.report') AND ${a}.payload->>'fingerprint' ~ '^[a-f0-9]{64}$'
AND ${a}.provider_action_id LIKE 'speko_event:%' AND (
EXISTS (SELECT 1 FROM chat_voice_sessions session WHERE session.id::text = ${a}.payload->>'sessionId'
AND session.company_id = ${a}.company_id AND session.endpoint_id = ${a}.endpoint_id AND ${closedSession('session')})
OR (${a}.payload->'sessionId' = 'null'::jsonb AND EXISTS (
SELECT 1 FROM chat_voice_inbound_calls admission WHERE admission.company_id = ${a}.company_id
AND admission.endpoint_id = ${a}.endpoint_id AND admission.state IN ('denied', 'expired', 'ended')
AND admission.cleanup_confirmed_at IS NOT NULL AND admission.cleanup_next_attempt_at IS NULL))))
OR (${a}.kind = 'speko_voice_reply_push' AND (${submitted(a)})
AND ${a}.payload->>'textDigest' ~ '^[a-f0-9]{64}$'
AND ${a}.payload->>'partIndex' ~ '^[0-9]+$' AND ${a}.payload->>'partCount' ~ '^[1-9][0-9]*$'
AND CASE WHEN ${a}.payload->>'partIndex' ~ '^[0-9]+$' AND ${a}.payload->>'partCount' ~ '^[1-9][0-9]*$'
THEN (${a}.payload->>'partIndex')::numeric < (${a}.payload->>'partCount')::numeric ELSE false END
AND EXISTS (SELECT 1 FROM chat_voice_sessions session JOIN chat_voice_replies reply
ON reply.session_id = session.id AND reply.company_id = session.company_id
JOIN chat_publications publication ON publication.id = reply.publication_id AND publication.company_id = reply.company_id
WHERE session.id::text = ${a}.payload->>'sessionId' AND session.company_id = ${a}.company_id
AND session.endpoint_id = ${a}.endpoint_id AND session.conversation_id = ${a}.conversation_id
AND session.provider_session_id = ${a}.payload->>'providerSessionId' AND session.generation::text = ${a}.payload->>'generation'
AND reply.id::text = ${a}.payload->>'replyId' AND publication.id::text = ${a}.payload->>'publicationId'
AND ${a}.provider_action_id = 'voice_reply_push:' || reply.id::text || CASE WHEN ${a}.payload->>'partIndex' = '0'
THEN '' ELSE ':part:' || (${a}.payload->>'partIndex') END
AND publication.endpoint_id = session.endpoint_id AND publication.conversation_id = session.conversation_id
AND publication.issue_id = session.issue_id AND publication.state = 'published' AND publication.published_at IS NOT NULL AND ${closedSession('session')})))`;
}
@@ -13,7 +13,8 @@ import { SpekoProviderError } from "./speko-provider.js";
type Entry = { id: string; companyId: string; endpointId: string; connectionId: string; dueAt: number };
type Report = SpekoCallReport;
const WINDOW_MS = 24 * 60 * 60 * 1000;
export const VOICE_CALL_REPORT_WINDOW_MS = 24 * 60 * 60 * 1000;
const WINDOW_MS = VOICE_CALL_REPORT_WINDOW_MS;
const eligible = (now: number) => and(inArray(chatVoiceSessions.state, ["ended", "failed", "expired"]),
isNotNull(chatVoiceSessions.providerSessionId), gte(chatVoiceSessions.endedAt, new Date(now - WINDOW_MS)));
+7 -4
View File
@@ -30,8 +30,10 @@ function resolveWithin(baseDir: string, objectKey: string): string {
async function statOrNull(filePath: string) {
try {
return await fs.stat(filePath);
} catch {
return null;
} catch (error) {
const code = (error as NodeJS.ErrnoException | null)?.code;
if (code === "ENOENT" || code === "ENOTDIR") return null;
throw error;
}
}
@@ -89,8 +91,9 @@ export function createLocalDiskStorageProvider(baseDir: string): StorageProvider
const filePath = resolveWithin(root, input.objectKey);
try {
await fs.unlink(filePath);
} catch {
// idempotent delete
} catch (error) {
const code = (error as NodeJS.ErrnoException | null)?.code;
if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
}
},
};