mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
fix(server): wake milestones from GitHub repository restoration
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
bdf7ae7e10
commit
b634353059
4 files changed
+43
-18
No files matched your search
@@ -1127,3 +1127,28 @@ Final resource checkpoint: all 20 projection units pass at the corrected path in
|
||||
/tmp/sleep28-resource-build.log. Combined selected evidence is 116 distinct
|
||||
milestone/projection/provider cases, plus the 109 propagated configuration-owner
|
||||
cases. Full remote checks/reviews remain required for the new head.
|
||||
|
||||
## GitHub repository restoration follow-up
|
||||
|
||||
GitHub's signed repository callback has a separate restoration path. Its real
|
||||
owner regression fails in /tmp/sleep28-resource-github-before.log. The provider
|
||||
module now receives a narrow restoreResourceConversations callback from the
|
||||
existing resource owner, using the same transaction and issue notification.
|
||||
Provider payload parsing stays in github/resources.ts. All three resource cases
|
||||
pass in /tmp/sleep28-resource-github-tests.log; five GitHub integration cases
|
||||
pass in /tmp/sleep28-resource-github-acceptance.log, including rename/transfer,
|
||||
concurrent installation events, foreign-installation canonical inventory,
|
||||
credential rotation, and paused endpoints. Server build/typecheck passes in
|
||||
/tmp/sleep28-resource-github-build.log. Installation recovery already invokes
|
||||
reconcileProviderResourceRows; its restoration uses the covered inventory path.
|
||||
|
||||
Next concrete producer: telegram/resources.ts::migrateTelegramResourceConversations
|
||||
sets moved conversations active before the generic availability transition runs,
|
||||
so that transition cannot discover the earlier unavailable state. Preserve the
|
||||
existing migration semantics and notify its affected issue scopes before the
|
||||
migration write. Normal chat admission also writes links and activates the
|
||||
conversation in chat-channels.ts; establish its wake ordering explicitly. The
|
||||
Slack waiting-to-active helpers preserve selector eligibility (both states are
|
||||
already admitted). Email uses explicit publication mode, which the lifecycle
|
||||
selector and lineage resolver exclude. These dispositions narrow the remaining
|
||||
configuration audit; they do not authorize removing the periodic scan yet.
|
||||
@@ -49,16 +49,20 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
}
|
||||
const publications = () => db.select().from(chatPublications).where(eq(chatPublications.issueId, f.issueId));
|
||||
|
||||
it.each(["inventory", "lifecycle"] as const)("wakes only reopened conversations after %s commits, preserving rollback and completed history", async path => {
|
||||
it.each(["inventory", "lifecycle", "github repository"] as const)("wakes only reopened conversations after %s commits, preserving rollback and completed history", async path => {
|
||||
const provider = path === "github repository" ? "github" : "slack";
|
||||
const resourceType = provider === "github" ? "repository" : "channel";
|
||||
const providerResourceId = provider === "github" ? "owner/repo" : "recovered-channel";
|
||||
await db.update(chatEndpoints).set({ provider }).where(eq(chatEndpoints.id, f.endpointId));
|
||||
const runId = randomUUID();
|
||||
await db.insert(heartbeatRuns).values({ id: runId, companyId: f.companyId, agentId: f.agentId,
|
||||
responsibleUserId: f.userId, status: "failed", errorCode: "test_run_failed", updatedAt: new Date(),
|
||||
contextSnapshot: { issueId: f.issueId, source: "chat:slack", endpointId: f.endpointId, wakeCommentId: f.commentId },
|
||||
contextSnapshot: { issueId: f.issueId, source: `chat:${provider}`, endpointId: f.endpointId, wakeCommentId: f.commentId },
|
||||
});
|
||||
await db.update(issues).set({ executionRunId: runId }).where(eq(issues.id, f.issueId));
|
||||
const [endpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpointId));
|
||||
const [resource] = await db.insert(chatEndpointResources).values({ companyId: f.companyId, endpointId: f.endpointId,
|
||||
type: "channel", providerResourceId: "recovered-channel", label: "Recovered", availability: "unavailable", enabled: true,
|
||||
type: resourceType, providerResourceId, label: "Recovered", availability: "unavailable", enabled: true,
|
||||
}).returning();
|
||||
await db.update(chatConversations).set({ resourceId: resource.id, state: "unavailable" }).where(eq(chatConversations.id, f.conversationId));
|
||||
const completedId = randomUUID();
|
||||
@@ -76,7 +80,9 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
endpointRecord: async () => ({ endpoint }), withCredentialMutationLease: withLease, runtimeGeneration: () => 0, maxErrorText: 1000,
|
||||
});
|
||||
if (path === "inventory") {
|
||||
await resources.reconcileProviderResourceRows(endpoint, { provider: "slack", resources: [{ type: "channel", providerResourceId: "recovered-channel", label: "Recovered" }] }, lease);
|
||||
await resources.reconcileProviderResourceRows(endpoint, { provider, resources: [{ type: resourceType, providerResourceId, label: "Recovered" }] }, lease);
|
||||
} else if (path === "github repository") {
|
||||
await resources.reconcileGitHubWebhookRepository(endpoint, { repository: { id: 123, full_name: providerResourceId, name: "repo", owner: { id: 321, login: "owner" } } }, lease);
|
||||
} else {
|
||||
await resources.applyProviderAvailability({ currentEndpoint: endpoint, credentialLease: lease, staleResourceEffect: false,
|
||||
claimed: delivery, candidate: { attempts: 0 }, effect: { kind: "resource", provider: "slack", providerEventId: delivery.providerEventId,
|
||||
@@ -99,7 +105,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
||||
try {
|
||||
await withLease(endpoint, lease => createChatResourceReconciliation(db, {
|
||||
endpointRecord: async () => ({ endpoint }), withCredentialMutationLease: withLease, runtimeGeneration: () => 0, maxErrorText: 1000,
|
||||
}).reconcileProviderResourceRows(endpoint, { provider: "slack", resources: [{ type: "channel", providerResourceId: "recovered-channel", label: "Recovered" }] }, lease));
|
||||
}).reconcileProviderResourceRows(endpoint, { provider, resources: [{ type: resourceType, providerResourceId, label: "Recovered" }] }, lease));
|
||||
expect(notices).toEqual([]);
|
||||
} finally { unsubscribe(); }
|
||||
await flush(); await quiet();
|
||||
|
||||
@@ -201,11 +201,12 @@ export async function reconcileGitHubRepositoryCoordinates(tx: DbTransaction, en
|
||||
type GitHubResourceOptions = {
|
||||
endpointRecord: (endpointId: string) => Promise<{ endpoint: EndpointRow } | null>;
|
||||
upsertProviderResourceRow: (tx: DbTransaction, endpoint: EndpointRow, item: ChatProviderResourceInventoryItem) => Promise<{ id: string } | null>;
|
||||
restoreResourceConversations: (tx: DbTransaction, endpoint: EndpointRow, resourceId: string) => Promise<void>;
|
||||
updateResourceSelection: (endpointId: string, updates: Array<{ id: string; enabled: boolean }>, actorUserId?: string | null, options?: { allGitHubRepositoriesEnabled?: boolean }) => Promise<void>;
|
||||
};
|
||||
|
||||
export function createGitHubResources(db: Db, options: GitHubResourceOptions) {
|
||||
const { endpointRecord, upsertProviderResourceRow, updateResourceSelection } = options;
|
||||
const { endpointRecord, upsertProviderResourceRow, restoreResourceConversations, updateResourceSelection } = options;
|
||||
async function reconcileGitHubWebhookRepository(
|
||||
endpoint: EndpointRow,
|
||||
payload: unknown,
|
||||
@@ -220,17 +221,7 @@ export function createGitHubResources(db: Db, options: GitHubResourceOptions) {
|
||||
// A correctly signed repository callback is current provider proof. Keep
|
||||
// Paperclip's enabled choice, and reopen only conversations previously
|
||||
// quarantined for provider availability loss.
|
||||
await tx
|
||||
.update(chatConversations)
|
||||
.set({ state: "active", updatedAt: new Date() })
|
||||
.where(
|
||||
and(
|
||||
eq(chatConversations.companyId, endpoint.companyId),
|
||||
eq(chatConversations.endpointId, endpoint.id),
|
||||
eq(chatConversations.resourceId, resource.id),
|
||||
eq(chatConversations.state, "unavailable"),
|
||||
),
|
||||
);
|
||||
await restoreResourceConversations(tx, endpoint, resource.id);
|
||||
await credentialLease.assertOwned(tx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -604,7 +604,10 @@ export function createChatResourceReconciliation(db: Db, options: ResourceReconc
|
||||
});
|
||||
}
|
||||
|
||||
const { reconcileGitHubWebhookRepository, listGitHubRepositories, toggleAllGitHubRepositories } = createGitHubResources(db, { endpointRecord, upsertProviderResourceRow, updateResourceSelection });
|
||||
const { reconcileGitHubWebhookRepository, listGitHubRepositories, toggleAllGitHubRepositories } = createGitHubResources(db, {
|
||||
endpointRecord, upsertProviderResourceRow, updateResourceSelection,
|
||||
restoreResourceConversations: (tx, endpoint, resourceId) => setResourceConversationAvailability(tx, endpoint, resourceId, true, new Date()),
|
||||
});
|
||||
|
||||
return { reconcileGitHubWebhookRepository, reconcileProviderResourceRows, listResources, listGitHubRepositories, replaceResources, toggleAllGitHubRepositories, hasNewerProcessedProviderLifecycleEffect, applyProviderAvailability };
|
||||
}
|
||||
Reference in new issue
Block a user