mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +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
|
/tmp/sleep28-resource-build.log. Combined selected evidence is 116 distinct
|
||||||
milestone/projection/provider cases, plus the 109 propagated configuration-owner
|
milestone/projection/provider cases, plus the 109 propagated configuration-owner
|
||||||
cases. Full remote checks/reviews remain required for the new head.
|
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));
|
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();
|
const runId = randomUUID();
|
||||||
await db.insert(heartbeatRuns).values({ id: runId, companyId: f.companyId, agentId: f.agentId,
|
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(),
|
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));
|
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 [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,
|
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();
|
}).returning();
|
||||||
await db.update(chatConversations).set({ resourceId: resource.id, state: "unavailable" }).where(eq(chatConversations.id, f.conversationId));
|
await db.update(chatConversations).set({ resourceId: resource.id, state: "unavailable" }).where(eq(chatConversations.id, f.conversationId));
|
||||||
const completedId = randomUUID();
|
const completedId = randomUUID();
|
||||||
@@ -76,7 +80,9 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
|
|||||||
endpointRecord: async () => ({ endpoint }), withCredentialMutationLease: withLease, runtimeGeneration: () => 0, maxErrorText: 1000,
|
endpointRecord: async () => ({ endpoint }), withCredentialMutationLease: withLease, runtimeGeneration: () => 0, maxErrorText: 1000,
|
||||||
});
|
});
|
||||||
if (path === "inventory") {
|
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 {
|
} else {
|
||||||
await resources.applyProviderAvailability({ currentEndpoint: endpoint, credentialLease: lease, staleResourceEffect: false,
|
await resources.applyProviderAvailability({ currentEndpoint: endpoint, credentialLease: lease, staleResourceEffect: false,
|
||||||
claimed: delivery, candidate: { attempts: 0 }, effect: { kind: "resource", provider: "slack", providerEventId: delivery.providerEventId,
|
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 {
|
try {
|
||||||
await withLease(endpoint, lease => createChatResourceReconciliation(db, {
|
await withLease(endpoint, lease => createChatResourceReconciliation(db, {
|
||||||
endpointRecord: async () => ({ endpoint }), withCredentialMutationLease: withLease, runtimeGeneration: () => 0, maxErrorText: 1000,
|
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([]);
|
expect(notices).toEqual([]);
|
||||||
} finally { unsubscribe(); }
|
} finally { unsubscribe(); }
|
||||||
await flush(); await quiet();
|
await flush(); await quiet();
|
||||||
|
|||||||
@@ -201,11 +201,12 @@ export async function reconcileGitHubRepositoryCoordinates(tx: DbTransaction, en
|
|||||||
type GitHubResourceOptions = {
|
type GitHubResourceOptions = {
|
||||||
endpointRecord: (endpointId: string) => Promise<{ endpoint: EndpointRow } | null>;
|
endpointRecord: (endpointId: string) => Promise<{ endpoint: EndpointRow } | null>;
|
||||||
upsertProviderResourceRow: (tx: DbTransaction, endpoint: EndpointRow, item: ChatProviderResourceInventoryItem) => Promise<{ id: string } | 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>;
|
updateResourceSelection: (endpointId: string, updates: Array<{ id: string; enabled: boolean }>, actorUserId?: string | null, options?: { allGitHubRepositoriesEnabled?: boolean }) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createGitHubResources(db: Db, options: GitHubResourceOptions) {
|
export function createGitHubResources(db: Db, options: GitHubResourceOptions) {
|
||||||
const { endpointRecord, upsertProviderResourceRow, updateResourceSelection } = options;
|
const { endpointRecord, upsertProviderResourceRow, restoreResourceConversations, updateResourceSelection } = options;
|
||||||
async function reconcileGitHubWebhookRepository(
|
async function reconcileGitHubWebhookRepository(
|
||||||
endpoint: EndpointRow,
|
endpoint: EndpointRow,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
@@ -220,17 +221,7 @@ export function createGitHubResources(db: Db, options: GitHubResourceOptions) {
|
|||||||
// A correctly signed repository callback is current provider proof. Keep
|
// A correctly signed repository callback is current provider proof. Keep
|
||||||
// Paperclip's enabled choice, and reopen only conversations previously
|
// Paperclip's enabled choice, and reopen only conversations previously
|
||||||
// quarantined for provider availability loss.
|
// quarantined for provider availability loss.
|
||||||
await tx
|
await restoreResourceConversations(tx, endpoint, resource.id);
|
||||||
.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 credentialLease.assertOwned(tx);
|
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 };
|
return { reconcileGitHubWebhookRepository, reconcileProviderResourceRows, listResources, listGitHubRepositories, replaceResources, toggleAllGitHubRepositories, hasNewerProcessedProviderLifecycleEffect, applyProviderAvailability };
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user