@opengeni/db 5.0.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/automations.d.ts +0 -2
  2. package/dist/canonical-human-identities.d.ts +4 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-components.d.ts +5 -13
  5. package/dist/capability-integrations.d.ts +23 -8
  6. package/dist/capability-reconciliation.d.ts +5 -0
  7. package/dist/capability-reconciliation.js +21 -0
  8. package/dist/capability-reconciliation.js.map +1 -0
  9. package/dist/{chunk-6T2GGYWE.js → chunk-4UJCXBR2.js} +4 -5800
  10. package/dist/chunk-4UJCXBR2.js.map +1 -0
  11. package/dist/{chunk-MWMXWHJD.js → chunk-7JCDSMX2.js} +2 -2
  12. package/dist/chunk-AJA2ZIUW.js +219 -0
  13. package/dist/chunk-AJA2ZIUW.js.map +1 -0
  14. package/dist/chunk-CDAT6EKW.js +247 -0
  15. package/dist/chunk-CDAT6EKW.js.map +1 -0
  16. package/dist/{chunk-DPONPYJD.js → chunk-CJI6G254.js} +70 -18
  17. package/dist/chunk-CJI6G254.js.map +1 -0
  18. package/dist/chunk-G32NVMRJ.js +276 -0
  19. package/dist/chunk-G32NVMRJ.js.map +1 -0
  20. package/dist/chunk-GABXCG3N.js +5811 -0
  21. package/dist/chunk-GABXCG3N.js.map +1 -0
  22. package/dist/{chunk-ERKVSPDX.js → chunk-LVY33EWL.js} +3 -3
  23. package/dist/{chunk-45CP2YKY.js → chunk-MAMBI2HR.js} +9 -3
  24. package/dist/{chunk-45CP2YKY.js.map → chunk-MAMBI2HR.js.map} +1 -1
  25. package/dist/{chunk-F73UKJY6.js → chunk-MN5PZMEK.js} +91 -108
  26. package/dist/chunk-MN5PZMEK.js.map +1 -0
  27. package/dist/chunk-PJI4MUZ7.js +131 -0
  28. package/dist/chunk-PJI4MUZ7.js.map +1 -0
  29. package/dist/{chunk-T6BWIGWJ.js → chunk-RIYHP2WU.js} +2 -2
  30. package/dist/{chunk-7HMK3B45.js → chunk-RL2MUXM3.js} +16 -7
  31. package/dist/chunk-RL2MUXM3.js.map +1 -0
  32. package/dist/{chunk-HQDVI2DB.js → chunk-SYKHLPTK.js} +4 -3
  33. package/dist/{chunk-HQDVI2DB.js.map → chunk-SYKHLPTK.js.map} +1 -1
  34. package/dist/{chunk-T6YLHJLS.js → chunk-XIXVVBW2.js} +198 -51
  35. package/dist/chunk-XIXVVBW2.js.map +1 -0
  36. package/dist/chunk-ZPJ6V2VG.js +156 -0
  37. package/dist/chunk-ZPJ6V2VG.js.map +1 -0
  38. package/dist/codex-capacity-recovery.d.ts +12 -0
  39. package/dist/codex-failure-eligibility.d.ts +9 -0
  40. package/dist/company-profile-schema.d.ts +4 -2
  41. package/dist/connect-attempts.d.ts +15 -0
  42. package/dist/connect-attempts.js +25 -0
  43. package/dist/connect-attempts.js.map +1 -0
  44. package/dist/connection-authority.d.ts +7 -63
  45. package/dist/connection-refresh-lock.d.ts +4 -0
  46. package/dist/connection-token-resolver.d.ts +8 -43
  47. package/dist/database-timing.d.ts +10 -0
  48. package/dist/database.d.ts +4 -0
  49. package/dist/editable-artifact-durable-export.js +2 -2
  50. package/dist/editable-artifacts.d.ts +9 -0
  51. package/dist/editable-artifacts.js +7 -5
  52. package/dist/index.d.ts +447 -78
  53. package/dist/index.js +12705 -12872
  54. package/dist/index.js.map +1 -1
  55. package/dist/insights-usage-bundle.d.ts +3 -0
  56. package/dist/integration-bindings.d.ts +1 -1
  57. package/dist/integration-facets.d.ts +18 -1
  58. package/dist/knowledge-entries-schema.d.ts +2 -0
  59. package/dist/knowledge-entries.d.ts +1 -0
  60. package/dist/knowledge-indexing.d.ts +1 -0
  61. package/dist/live-session-attempt.d.ts +35 -137
  62. package/dist/managed-auth-session-sets.js +3 -3
  63. package/dist/mcp-account-bindings.d.ts +4 -0
  64. package/dist/mcp-oauth.d.ts +11 -0
  65. package/dist/mcp-operations.js +2 -2
  66. package/dist/migrate.js +1 -1
  67. package/dist/organization-integration-policy.d.ts +51 -0
  68. package/dist/organization-integration-policy.js +18 -0
  69. package/dist/organization-integration-policy.js.map +1 -0
  70. package/dist/organization-usage.d.ts +20 -0
  71. package/dist/plugin-packages.d.ts +9 -12
  72. package/dist/pr-review.d.ts +0 -11
  73. package/dist/preference-registry-schema.d.ts +17 -0
  74. package/dist/provision-roles.js +1 -1
  75. package/dist/retained-provider-commands.d.ts +124 -6
  76. package/dist/retained-provider-commands.js +29 -97
  77. package/dist/retained-provider-commands.js.map +1 -1
  78. package/dist/retired-session-create-metadata.d.ts +4 -0
  79. package/dist/runtime-posture.d.ts +7 -5
  80. package/dist/schema.d.ts +876 -656
  81. package/dist/schema.js +5 -7
  82. package/dist/session-admission-block.d.ts +32 -0
  83. package/dist/session-attempt-writers.d.ts +4 -0
  84. package/dist/session-background-commands.js +5 -4
  85. package/dist/session-command-output.js +8 -126
  86. package/dist/session-command-output.js.map +1 -1
  87. package/dist/session-control.d.ts +5 -1
  88. package/dist/session-event-slices.d.ts +10 -0
  89. package/dist/session-event-slices.js +8 -222
  90. package/dist/session-event-slices.js.map +1 -1
  91. package/dist/session-file-attachments.d.ts +28 -0
  92. package/dist/session-goal-reports.d.ts +43 -0
  93. package/dist/session-mcp-approval.d.ts +4 -0
  94. package/dist/session-mcp-credential-rotation.js +5 -4
  95. package/dist/session-mcp-credential-rotation.js.map +1 -1
  96. package/dist/session-message-search.d.ts +22 -0
  97. package/dist/session-queue-commands.d.ts +2 -6
  98. package/dist/session-retry.d.ts +36 -0
  99. package/dist/session-tenancy.js +3 -3
  100. package/dist/skill-source-release-impact.d.ts +18 -0
  101. package/dist/skill-source-release.d.ts +9 -0
  102. package/dist/skills.d.ts +7 -1
  103. package/dist/user-resource-authority.d.ts +0 -8
  104. package/dist/video-generation.js +3 -3
  105. package/dist/workspace-tool-defaults.js +5 -4
  106. package/dist/workspace-tool-defaults.js.map +1 -1
  107. package/drizzle/0468_knowledge_relationship_projection.sql +30 -0
  108. package/drizzle/0469_knowledge_source_discovery.sql +85 -0
  109. package/drizzle/0470_organization_integration_policy.sql +151 -0
  110. package/drizzle/0471_usage_events_account_recent_index.sql +4 -0
  111. package/drizzle/0472_usage_events_workspace_recent_index.sql +4 -0
  112. package/drizzle/0473_organization_usage_analytical_capability.sql +237 -0
  113. package/drizzle/0474_goal_report_requirements.sql +156 -0
  114. package/drizzle/0475_session_event_history_statistics.sql +15 -0
  115. package/drizzle/0476_session_event_history_policy_planning.sql +29 -0
  116. package/drizzle/0477_managed_sign_in_methods.sql +303 -0
  117. package/drizzle/0478_sender_owned_connections.sql +2772 -0
  118. package/drizzle/0479_connection_create_idempotency.sql +16 -0
  119. package/drizzle/0480_private_session_organization_key_administration.sql +154 -0
  120. package/drizzle/0481_personal_connection_visibility_cleanup.sql +45 -0
  121. package/drizzle/0482_remove_packs.sql +417 -0
  122. package/drizzle/0483_preclaim_admission_block.sql +85 -0
  123. package/drizzle/0484_insights_usage_projection.sql +211 -0
  124. package/drizzle/0485_inherited_mcp_approval_policies.sql +6 -0
  125. package/drizzle/0486_reasoning_configuration_message_forks.sql +35 -0
  126. package/drizzle/0487_skill_catalog_message_forks.sql +26 -0
  127. package/drizzle/0488_permanent_skill_removal.sql +207 -0
  128. package/drizzle/0489_modal_command_byte_offsets.sql +17 -0
  129. package/drizzle/0490_periodic_capture_attempt_cadence.sql +6 -0
  130. package/drizzle/0491_warm_capture_holder_reclamation.sql +57 -0
  131. package/drizzle/0492_codex_accepted_source_authority.sql +249 -0
  132. package/drizzle/0493_stopping_command_error_containment.sql +58 -0
  133. package/drizzle/0494_mcp_account_bindings.sql +334 -0
  134. package/drizzle/0495_consented_sandbox_recovery.sql +220 -0
  135. package/drizzle/0496_supervised_command_settlement.sql +386 -0
  136. package/drizzle/0497_modal_inventory_owner_read_capability.sql +112 -0
  137. package/drizzle/0498_enrollment_membership_fence.sql +87 -0
  138. package/drizzle/0499_session_attachment_access.sql +224 -0
  139. package/drizzle/0500_session_execution_epoch.sql +37 -0
  140. package/drizzle/0501_session_sharing_execution.sql +591 -0
  141. package/drizzle/0502_active_message_boundary_forks.sql +30 -0
  142. package/package.json +17 -5
  143. package/src/automations.ts +17 -73
  144. package/src/canonical-human-identities.ts +17 -5
  145. package/src/capability-components.ts +7 -24
  146. package/src/capability-integrations.ts +301 -21
  147. package/src/capability-reconciliation.ts +19 -0
  148. package/src/codex-capacity-recovery.ts +47 -0
  149. package/src/codex-failure-eligibility.ts +93 -0
  150. package/src/connect-attempts.ts +49 -18
  151. package/src/connection-authority.ts +12 -158
  152. package/src/connection-refresh-lock.ts +22 -0
  153. package/src/connection-token-resolver.ts +58 -530
  154. package/src/database-timing.ts +43 -0
  155. package/src/database.ts +61 -26
  156. package/src/editable-artifacts.ts +1 -1
  157. package/src/index.ts +3578 -1273
  158. package/src/insights-model-bundle.ts +19 -7
  159. package/src/insights-usage-bundle.ts +16 -19
  160. package/src/integration-bindings.ts +2 -7
  161. package/src/integration-facets.ts +73 -6
  162. package/src/knowledge-entries.ts +8 -1
  163. package/src/live-session-attempt.ts +0 -22
  164. package/src/mcp-account-bindings.ts +12 -0
  165. package/src/mcp-oauth.ts +26 -0
  166. package/src/organization-integration-policy.ts +235 -0
  167. package/src/organization-usage.ts +83 -0
  168. package/src/plugin-packages.ts +226 -42
  169. package/src/pr-review.ts +7 -69
  170. package/src/preference-registry-schema.ts +1 -0
  171. package/src/provision-roles.ts +50 -37
  172. package/src/retained-provider-commands.ts +286 -9
  173. package/src/retired-session-create-metadata.ts +16 -0
  174. package/src/runtime-posture.ts +217 -13
  175. package/src/schema.ts +104 -120
  176. package/src/session-admission-block.ts +29 -0
  177. package/src/session-attempt-writers.ts +48 -0
  178. package/src/session-control.ts +21 -5
  179. package/src/session-event-slices.ts +76 -26
  180. package/src/session-file-attachments.ts +66 -0
  181. package/src/session-goal-reports.ts +255 -0
  182. package/src/session-mcp-approval.ts +30 -0
  183. package/src/session-message-search.ts +348 -0
  184. package/src/session-queue-commands.ts +60 -36
  185. package/src/session-retry.ts +319 -0
  186. package/src/skill-config-migration.ts +11 -2
  187. package/src/skill-source-release-impact.ts +33 -0
  188. package/src/skill-source-release.ts +67 -26
  189. package/src/skills.ts +44 -6
  190. package/src/user-resource-authority.ts +0 -56
  191. package/dist/chunk-6T2GGYWE.js.map +0 -1
  192. package/dist/chunk-7HMK3B45.js.map +0 -1
  193. package/dist/chunk-DPONPYJD.js.map +0 -1
  194. package/dist/chunk-F73UKJY6.js.map +0 -1
  195. package/dist/chunk-T6YLHJLS.js.map +0 -1
  196. package/dist/host-mcp-binding-match.d.ts +0 -5
  197. package/dist/host-mcp-bindings.d.ts +0 -79
  198. package/dist/host-mcp-resolvers.d.ts +0 -47
  199. package/dist/host-mcp-task-authority.d.ts +0 -43
  200. package/dist/host-selection-identity.d.ts +0 -6
  201. package/dist/pack-components.d.ts +0 -83
  202. package/dist/pack-installations.d.ts +0 -86
  203. package/src/host-mcp-binding-match.ts +0 -34
  204. package/src/host-mcp-bindings.ts +0 -777
  205. package/src/host-mcp-resolvers.ts +0 -266
  206. package/src/host-mcp-task-authority.ts +0 -384
  207. package/src/host-selection-identity.ts +0 -29
  208. package/src/pack-components.ts +0 -980
  209. package/src/pack-installations.ts +0 -950
  210. /package/dist/{chunk-MWMXWHJD.js.map → chunk-7JCDSMX2.js.map} +0 -0
  211. /package/dist/{chunk-ERKVSPDX.js.map → chunk-LVY33EWL.js.map} +0 -0
  212. /package/dist/{chunk-T6BWIGWJ.js.map → chunk-RIYHP2WU.js.map} +0 -0
@@ -0,0 +1,319 @@
1
+ import {
2
+ metadataWithTurnExecutionPolicyV1,
3
+ type SessionRetryRequest,
4
+ type SessionRetryResponse,
5
+ SessionRetryResponse as SessionRetryResponseSchema,
6
+ type TurnExecutionPolicyV1,
7
+ } from "@opengeni/contracts";
8
+ import { and, desc, eq, sql } from "drizzle-orm";
9
+ import { rawRows, type Database, type SessionActivityDatabase } from "./database";
10
+ import * as schema from "./schema";
11
+ import { clearCodexCapacityRecovery } from "./codex-capacity-recovery";
12
+ import { fromPostgresLosslessJson, withLosslessContentWriteVersion } from "./lossless-json";
13
+ import { sessionAttemptPendingWritersSql } from "./session-attempt-writers";
14
+ import {
15
+ canonicalSessionCommandHash,
16
+ evaluateSessionControl,
17
+ lockSessionEventWriteRows,
18
+ registerSessionWorkflowWakeInTransaction,
19
+ reserveSessionCommandReceipt,
20
+ updateSessionCommandReceiptResult,
21
+ SessionCommandIdempotencyError,
22
+ } from "./session-control";
23
+
24
+ export class SessionRetryConflictError extends Error {
25
+ constructor(
26
+ readonly code:
27
+ | "RETRY_STALE_FAILURE"
28
+ | "RETRY_EXECUTION_UNRESOLVED"
29
+ | "RETRY_PAUSED"
30
+ | "RETRY_SANDBOX_RECOVERY_REQUIRED"
31
+ | "RETRY_UNSUPPORTED_FAILURE",
32
+ message: string,
33
+ ) {
34
+ super(message);
35
+ this.name = "SessionRetryConflictError";
36
+ }
37
+ }
38
+
39
+ /** Authorized receipt reads precede mutable model/billing admission checks. */
40
+ export async function getSessionRetryReceiptInTransaction(
41
+ db: Database,
42
+ input: {
43
+ workspaceId: string;
44
+ sessionId: string;
45
+ subjectId: string;
46
+ request: SessionRetryRequest;
47
+ },
48
+ ): Promise<SessionRetryResponse | null> {
49
+ const [receipt] = await db
50
+ .select()
51
+ .from(schema.sessionCommandReceipts)
52
+ .where(
53
+ and(
54
+ eq(schema.sessionCommandReceipts.workspaceId, input.workspaceId),
55
+ eq(schema.sessionCommandReceipts.targetSessionId, input.sessionId),
56
+ eq(schema.sessionCommandReceipts.actorType, "human"),
57
+ eq(schema.sessionCommandReceipts.actorSubjectId, input.subjectId),
58
+ eq(schema.sessionCommandReceipts.action, "session.retry"),
59
+ eq(schema.sessionCommandReceipts.operationKey, input.request.clientEventId),
60
+ ),
61
+ )
62
+ .limit(1);
63
+ if (!receipt) return null;
64
+ if (receipt.canonicalRequestHash !== canonicalSessionCommandHash(input.request))
65
+ throw new SessionCommandIdempotencyError();
66
+ return { ...SessionRetryResponseSchema.parse(receipt.result), outcome: "replayed" };
67
+ }
68
+
69
+ type EffectiveSandboxRecoveryBlocked = (
70
+ db: Database,
71
+ session: typeof schema.sessions.$inferSelect,
72
+ ) => Promise<boolean>;
73
+
74
+ /** The root composition binds the canonical effective-route guard. Keep this
75
+ * leaf independent of the root barrel; there is no permissive default. */
76
+ export function createRetryFailedSessionInTransaction(
77
+ sessionEffectiveSandboxRecoveryBlocked: EffectiveSandboxRecoveryBlocked,
78
+ ) {
79
+ return (
80
+ db: SessionActivityDatabase,
81
+ input: Parameters<typeof retryFailedSessionInTransaction>[1],
82
+ ) => retryFailedSessionInTransaction(db, input, sessionEffectiveSandboxRecoveryBlocked);
83
+ }
84
+
85
+ /** Caller owns the tenant/activity transaction and authorization. No provider I/O. */
86
+ async function retryFailedSessionInTransaction(
87
+ db: SessionActivityDatabase,
88
+ input: {
89
+ accountId: string;
90
+ workspaceId: string;
91
+ sessionId: string;
92
+ subjectId: string;
93
+ request: SessionRetryRequest;
94
+ executionPolicy: TurnExecutionPolicyV1;
95
+ },
96
+ sessionEffectiveSandboxRecoveryBlocked: EffectiveSandboxRecoveryBlocked,
97
+ ): Promise<SessionRetryResponse & { eventIds: string[] }> {
98
+ const { accountId, workspaceId, sessionId, request } = input;
99
+ const locks = await lockSessionEventWriteRows(db, {
100
+ workspaceId,
101
+ controlLock: "share",
102
+ sessionIds: [sessionId],
103
+ });
104
+ const session = locks.sessions[0];
105
+ if (!session || session.accountId !== accountId)
106
+ throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "Session is unavailable");
107
+ const reserved = await reserveSessionCommandReceipt(db, {
108
+ accountId,
109
+ workspaceId,
110
+ targetSessionId: sessionId,
111
+ targetTurnId: null,
112
+ actor: { type: "human", subjectId: input.subjectId },
113
+ action: "session.retry",
114
+ operationKey: request.clientEventId,
115
+ canonicalRequestHash: canonicalSessionCommandHash(request),
116
+ });
117
+ if (reserved.replay)
118
+ return {
119
+ ...SessionRetryResponseSchema.parse(reserved.receipt.result),
120
+ outcome: "replayed",
121
+ eventIds: [],
122
+ };
123
+ const control = await evaluateSessionControl(db, workspaceId, sessionId, {
124
+ workspaceControl: locks.control ?? undefined,
125
+ });
126
+ if (control.state !== "active")
127
+ throw new SessionRetryConflictError(
128
+ "RETRY_PAUSED",
129
+ "Resume the deliberately paused workstream before retrying",
130
+ );
131
+ if (session.status !== "failed" || session.activeTurnId !== null)
132
+ throw new SessionRetryConflictError(
133
+ "RETRY_STALE_FAILURE",
134
+ "The failed session boundary changed",
135
+ );
136
+ const [failure] = await db
137
+ .select()
138
+ .from(schema.sessionEvents)
139
+ .where(
140
+ and(
141
+ eq(schema.sessionEvents.workspaceId, workspaceId),
142
+ eq(schema.sessionEvents.sessionId, sessionId),
143
+ sql`(${schema.sessionEvents.turnAssociation} is null or ${schema.sessionEvents.turnAssociation} = 'current')`,
144
+ sql`(${schema.sessionEvents.type} = 'turn.failed' or (${schema.sessionEvents.type} = 'session.status.changed' and ${schema.sessionEvents.payload}->>'status' = 'failed' and ${schema.sessionEvents.turnId} is null))`,
145
+ ),
146
+ )
147
+ .orderBy(desc(schema.sessionEvents.sequence))
148
+ .limit(1);
149
+ if (!failure || failure.id !== request.failureEventId)
150
+ throw new SessionRetryConflictError(
151
+ "RETRY_STALE_FAILURE",
152
+ "The failure event is no longer current",
153
+ );
154
+ const failurePayload = fromPostgresLosslessJson(failure.payload, failure.payloadCodecVersion);
155
+ if (
156
+ failurePayload !== null &&
157
+ typeof failurePayload === "object" &&
158
+ !Array.isArray(failurePayload) &&
159
+ (failurePayload as Record<string, unknown>).code === "provider_safety_refusal"
160
+ )
161
+ throw new SessionRetryConflictError(
162
+ "RETRY_UNSUPPORTED_FAILURE",
163
+ "A provider safety refusal cannot be retried as the same logical turn",
164
+ );
165
+ if (!failure.turnId)
166
+ throw new SessionRetryConflictError(
167
+ "RETRY_UNSUPPORTED_FAILURE",
168
+ "This failure has no retained logical turn to retry",
169
+ );
170
+ const [turn] = await db
171
+ .select()
172
+ .from(schema.sessionTurns)
173
+ .where(
174
+ and(
175
+ eq(schema.sessionTurns.workspaceId, workspaceId),
176
+ eq(schema.sessionTurns.sessionId, sessionId),
177
+ eq(schema.sessionTurns.id, failure.turnId),
178
+ ),
179
+ )
180
+ .for("update");
181
+ if (!turn || turn.status !== "failed" || turn.activeAttemptId !== null)
182
+ throw new SessionRetryConflictError("RETRY_STALE_FAILURE", "The failed turn changed");
183
+ if (failure.turnGeneration !== null && failure.turnGeneration !== turn.executionGeneration)
184
+ throw new SessionRetryConflictError(
185
+ "RETRY_STALE_FAILURE",
186
+ "The failed execution generation changed",
187
+ );
188
+ if (turn.scheduledTaskRunId !== null)
189
+ throw new SessionRetryConflictError(
190
+ "RETRY_UNSUPPORTED_FAILURE",
191
+ "A settled scheduled occurrence cannot be reopened as a human retry",
192
+ );
193
+ const [unresolved] = await rawRows<{ pending: boolean }>(
194
+ db,
195
+ sql`select (
196
+ exists (select 1 from session_turn_attempts attempt where attempt.workspace_id = ${workspaceId} and attempt.session_id = ${sessionId}
197
+ and (attempt.state <> 'closed' or ${sessionAttemptPendingWritersSql(sql`attempt`)}
198
+ or (attempt.quiesced_at is null and exists (select 1 from session_attempt_interruptions interruption where interruption.attempt_id = attempt.id))))
199
+ or exists (select 1 from session_pending_tool_calls pending where pending.workspace_id = ${workspaceId} and pending.session_id = ${sessionId})
200
+ or exists (select 1 from session_events tool_event where tool_event.workspace_id = ${workspaceId} and tool_event.session_id = ${sessionId} and tool_event.turn_id = ${turn.id}
201
+ and tool_event.type = 'agent.toolCall.output' and tool_event.payload->'recovery'->>'outcome' = 'unknown')
202
+ or exists (select 1 from session_turns live where live.workspace_id = ${workspaceId} and live.session_id = ${sessionId} and live.status in ('running', 'recovering', 'requires_action', 'waiting_capacity'))
203
+ ) as pending`,
204
+ );
205
+ if (unresolved?.pending)
206
+ throw new SessionRetryConflictError(
207
+ "RETRY_EXECUTION_UNRESOLVED",
208
+ "Execution or tool settlement is unresolved; retry cannot replay it",
209
+ );
210
+ const now = new Date();
211
+ if (await sessionEffectiveSandboxRecoveryBlocked(db, session))
212
+ throw new SessionRetryConflictError(
213
+ "RETRY_SANDBOX_RECOVERY_REQUIRED",
214
+ "The effective sandbox route requires recovery; review its checkpoint before retrying",
215
+ );
216
+ // A never-claimed prompt must traverse normal first claim exactly once so
217
+ // its original user history item is inserted. Started turns retain history.
218
+ const preclaim = turn.executionGeneration === 0;
219
+ const policy = input.executionPolicy;
220
+ const capacityRecoveryRetry =
221
+ failurePayload !== null &&
222
+ typeof failurePayload === "object" &&
223
+ !Array.isArray(failurePayload) &&
224
+ (failurePayload as Record<string, unknown>).code === "codex_capacity_recovery_exhausted";
225
+ // Only this explicitly authorized, exact failure-event Retry may replenish
226
+ // the breaker. Preserve accepted credential policy/refusal ledgers and all
227
+ // unrelated recovery budgets. Receipt replay returned above cannot reset it.
228
+ const retryMetadata = capacityRecoveryRetry
229
+ ? clearCodexCapacityRecovery(turn.metadata ?? {})
230
+ : turn.metadata;
231
+ if (capacityRecoveryRetry) {
232
+ await db
233
+ .update(schema.sessionGoals)
234
+ .set({ continuationSuppressedTurnId: null, updatedAt: now })
235
+ .where(
236
+ and(
237
+ eq(schema.sessionGoals.workspaceId, workspaceId),
238
+ eq(schema.sessionGoals.sessionId, sessionId),
239
+ eq(schema.sessionGoals.continuationSuppressedTurnId, turn.id),
240
+ ),
241
+ );
242
+ }
243
+ await db
244
+ .update(schema.sessionTurns)
245
+ .set({
246
+ status: preclaim ? "queued" : "recovering",
247
+ finishedAt: null,
248
+ updatedAt: now,
249
+ version: turn.version + 1,
250
+ model: policy.productModelId,
251
+ reasoningEffort: policy.reasoningEffort,
252
+ latencyMode: policy.latencyMode,
253
+ metadata: metadataWithTurnExecutionPolicyV1(retryMetadata, policy),
254
+ })
255
+ .where(eq(schema.sessionTurns.id, turn.id));
256
+ const events = await db
257
+ .insert(schema.sessionEvents)
258
+ .values(
259
+ withLosslessContentWriteVersion(
260
+ [
261
+ {
262
+ accountId,
263
+ workspaceId,
264
+ sessionId,
265
+ sequence: session.lastSequence + 1,
266
+ type: "turn.recovery.requested",
267
+ turnId: turn.id,
268
+ turnGeneration: turn.executionGeneration,
269
+ turnAssociation: "current",
270
+ payload: {
271
+ reason: "human_retry",
272
+ failureEventId: failure.id,
273
+ operationId: request.clientEventId,
274
+ model: policy.productModelId,
275
+ reasoningEffort: policy.reasoningEffort,
276
+ latencyMode: policy.latencyMode,
277
+ },
278
+ occurredAt: now,
279
+ },
280
+ {
281
+ accountId,
282
+ workspaceId,
283
+ sessionId,
284
+ sequence: session.lastSequence + 2,
285
+ type: "session.status.changed",
286
+ turnId: turn.id,
287
+ turnGeneration: turn.executionGeneration,
288
+ turnAssociation: "current",
289
+ payload: { status: preclaim ? "queued" : "recovering" },
290
+ occurredAt: now,
291
+ },
292
+ ],
293
+ "payload",
294
+ "payloadCodecVersion",
295
+ ),
296
+ )
297
+ .returning({ id: schema.sessionEvents.id });
298
+ await db
299
+ .update(schema.sessions)
300
+ .set({
301
+ status: preclaim ? "queued" : "recovering",
302
+ activeTurnId: preclaim ? null : turn.id,
303
+ admissionBlock: null,
304
+ lastSequence: session.lastSequence + 2,
305
+ queueVersion: session.queueVersion + 1,
306
+ updatedAt: now,
307
+ })
308
+ .where(eq(schema.sessions.id, sessionId));
309
+ await registerSessionWorkflowWakeInTransaction(db, {
310
+ accountId,
311
+ workspaceId,
312
+ sessionId,
313
+ temporalWorkflowId: session.temporalWorkflowId ?? `session-${sessionId}`,
314
+ reason: "human_failed_session_retry",
315
+ });
316
+ const response = { outcome: "accepted", turnId: turn.id, failureEventId: failure.id } as const;
317
+ await updateSessionCommandReceiptResult(db, reserved.receipt.id, { result: response });
318
+ return { ...response, eventIds: events.map((event) => event.id) };
319
+ }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
- StoredCapabilityPack,
4
3
  StoredSessionSkills,
4
+ StoredAutomationSessionTemplate,
5
5
  validateSkillTextFiles,
6
6
  type SkillFile,
7
7
  } from "@opengeni/contracts";
@@ -74,7 +74,16 @@ export function convertLegacyPackConfig(value: unknown, identity: string): Objec
74
74
  },
75
75
  };
76
76
  });
77
- StoredCapabilityPack.parse(converted);
77
+ // Historical migration 0433 only: validate the execution fields this
78
+ // conversion rewrites. The remaining archived manifest is opaque evidence,
79
+ // not an active capability or an admission contract.
80
+ if (converted.skills !== undefined) StoredSessionSkills.parse(converted.skills);
81
+ if (converted.automationTemplates !== undefined) {
82
+ if (!Array.isArray(converted.automationTemplates))
83
+ throw new Error("Expected historical automation template array");
84
+ for (const entry of converted.automationTemplates)
85
+ StoredAutomationSessionTemplate.parse(object(entry).sessionTemplate);
86
+ }
78
87
  return converted;
79
88
  }
80
89
 
@@ -0,0 +1,33 @@
1
+ export type SkillSourceReleaseHead = {
2
+ id: string;
3
+ account_id: string;
4
+ scope: string;
5
+ scope_workspace_id: string | null;
6
+ scope_version: number;
7
+ status: string;
8
+ active_revision_id: string | null;
9
+ provenance_source: string | null;
10
+ title: string | null;
11
+ };
12
+
13
+ type SkillRetentionReason = "customized" | "re_scoped";
14
+
15
+ /** One decision for both the user-visible impact and committed source release. */
16
+ export function classifySkillSourceRelease(
17
+ head: SkillSourceReleaseHead,
18
+ workspaceId: string,
19
+ ): {
20
+ disposition: "inactive" | "retained" | "removed";
21
+ retentionReasons: SkillRetentionReason[];
22
+ } {
23
+ if (head.status !== "active" || !head.active_revision_id)
24
+ return { disposition: "inactive", retentionReasons: [] };
25
+ const retentionReasons: SkillRetentionReason[] = [];
26
+ if (head.provenance_source !== "portable_skill") retentionReasons.push("customized");
27
+ if (head.scope !== "workspace" || head.scope_workspace_id !== workspaceId)
28
+ retentionReasons.push("re_scoped");
29
+ return {
30
+ disposition: retentionReasons.length ? "retained" : "removed",
31
+ retentionReasons,
32
+ };
33
+ }
@@ -2,6 +2,10 @@ import type { SkillActor } from "@opengeni/contracts";
2
2
  import { sql } from "drizzle-orm";
3
3
  import { rawRows, withWorkspaceSubjectRls, type Database } from "./database";
4
4
  import { deactivatePreferenceRegistry } from "./preference-registry";
5
+ import {
6
+ classifySkillSourceRelease,
7
+ type SkillSourceReleaseHead,
8
+ } from "./skill-source-release-impact";
5
9
 
6
10
  export type SkillSourceReleaseReceipt = {
7
11
  skillId: string;
@@ -11,6 +15,31 @@ export type SkillSourceReleaseReceipt = {
11
15
  warning: string | null;
12
16
  };
13
17
 
18
+ /** Caller supplies workspace/subject RLS; never bypass registry visibility for a preview. */
19
+ export async function readSkillSourceReleaseHeads(
20
+ db: Database,
21
+ workspaceId: string,
22
+ facetInstallationIds: readonly string[],
23
+ ): Promise<SkillSourceReleaseHead[]> {
24
+ if (!facetInstallationIds.length) return [];
25
+ return rawRows<SkillSourceReleaseHead>(
26
+ db,
27
+ sql`
28
+ SELECT DISTINCT h.id,h.account_id,h.scope,h.scope_workspace_id,h.scope_version,
29
+ h.status,h.active_revision_id,r.provenance_source,r.title
30
+ FROM skill_source_bindings b
31
+ JOIN capability_facet_installations fi ON fi.facet_id=b.skill_facet_id
32
+ AND fi.account_id=b.account_id AND fi.workspace_id=b.workspace_id
33
+ JOIN preference_registry_preferences h ON h.id=b.preference_id AND h.account_id=b.account_id
34
+ LEFT JOIN preference_registry_revisions r ON r.id=h.active_revision_id
35
+ AND r.preference_id=h.id AND r.account_id=h.account_id
36
+ WHERE b.workspace_id=${workspaceId}::uuid
37
+ AND fi.id=ANY(string_to_array(${facetInstallationIds.join(",")},',')::uuid[])
38
+ ORDER BY h.id
39
+ `,
40
+ );
41
+ }
42
+
14
43
  export class SkillSourceRemovalAuthorityError extends Error {
15
44
  readonly code = "skill_source_removal_requires_human";
16
45
  constructor() {
@@ -21,6 +50,14 @@ export class SkillSourceRemovalAuthorityError extends Error {
21
50
  }
22
51
  }
23
52
 
53
+ /** Never disclose the identity of a head absent from the caller's locked snapshot. */
54
+ export class SkillSourceReleaseSnapshotChangedError extends Error {
55
+ constructor() {
56
+ super("Skill source visibility changed during removal; review a refreshed preview.");
57
+ this.name = "SkillSourceReleaseSnapshotChangedError";
58
+ }
59
+ }
60
+
24
61
  /**
25
62
  * Called inside the source-owner removal transaction, with source installations
26
63
  * locked. Distribution identity/history stays bound after uninstall, so reinstall
@@ -28,7 +65,13 @@ export class SkillSourceRemovalAuthorityError extends Error {
28
65
  */
29
66
  export async function releaseOrphanedSkillHeads(
30
67
  db: Database,
31
- input: { workspaceId: string; facetInstallationIds: readonly string[]; skillActor?: SkillActor },
68
+ input: {
69
+ workspaceId: string;
70
+ facetInstallationIds: readonly string[];
71
+ skillActor?: SkillActor;
72
+ /** Exact heads already locked before the caller compared removal impact. */
73
+ lockedHeadIds?: readonly string[];
74
+ },
32
75
  ): Promise<SkillSourceReleaseReceipt[]> {
33
76
  if (!input.facetInstallationIds.length) return [];
34
77
  const bindings = await rawRows<{ id: string }>(
@@ -48,35 +91,37 @@ export async function releaseOrphanedSkillHeads(
48
91
  throw new SkillSourceRemovalAuthorityError();
49
92
  }
50
93
  return withWorkspaceSubjectRls(db, input.workspaceId, actor.subjectId, async (tx) => {
94
+ const visibleHeads = await readSkillSourceReleaseHeads(
95
+ tx,
96
+ input.workspaceId,
97
+ input.facetInstallationIds,
98
+ );
99
+ const lockedHeadIds = new Set(input.lockedHeadIds ?? visibleHeads.map((head) => head.id));
100
+ if (visibleHeads.some((head) => !lockedHeadIds.has(head.id)))
101
+ throw new SkillSourceReleaseSnapshotChangedError();
51
102
  // The established definer lock capability accepts at most two heads; acquire
52
103
  // individual heads in canonical order rather than widening its authority.
53
- for (const binding of bindings) {
104
+ // An inaccessible re-scoped head is preserved, never an excuse to acquire
105
+ // authority over another scope or disclose its identity in a receipt.
106
+ for (const binding of input.lockedHeadIds === undefined ? visibleHeads : []) {
54
107
  await tx.execute(
55
108
  sql`SELECT preference_id FROM preference_registry_lock_heads(ARRAY[${binding.id}::uuid])`,
56
109
  );
57
110
  }
58
- const rows = await rawRows<{
59
- id: string;
60
- account_id: string;
61
- scope: string;
62
- scope_workspace_id: string | null;
63
- scope_version: number;
64
- status: string;
65
- active_revision_id: string | null;
66
- provenance_source: string | null;
67
- }>(
111
+ const rows = await readSkillSourceReleaseHeads(
68
112
  tx,
69
- sql`
70
- SELECT h.id,h.account_id,h.scope,h.scope_workspace_id,h.scope_version,h.status,h.active_revision_id,r.provenance_source
71
- FROM preference_registry_preferences h LEFT JOIN preference_registry_revisions r
72
- ON r.id=h.active_revision_id AND r.preference_id=h.id AND r.account_id=h.account_id
73
- WHERE h.id=ANY(string_to_array(${bindings.map((binding) => binding.id).join(",")},',')::uuid[])
74
- ORDER BY h.id
75
- `,
113
+ input.workspaceId,
114
+ input.facetInstallationIds,
76
115
  );
116
+ // A second subject may have moved a previously invisible head into this
117
+ // scope since either read. Never widen the locked set or acquire a new head
118
+ // lock out of canonical order during cleanup.
119
+ if (rows.some((head) => !lockedHeadIds.has(head.id)))
120
+ throw new SkillSourceReleaseSnapshotChangedError();
77
121
  const receipts: SkillSourceReleaseReceipt[] = [];
78
122
  for (const head of rows) {
79
- if (head.status !== "active" || !head.active_revision_id) {
123
+ const impact = classifySkillSourceRelease(head, input.workspaceId);
124
+ if (impact.disposition === "inactive") {
80
125
  receipts.push({
81
126
  skillId: head.id,
82
127
  revisionId: head.active_revision_id,
@@ -84,11 +129,7 @@ export async function releaseOrphanedSkillHeads(
84
129
  eventId: null,
85
130
  warning: null,
86
131
  });
87
- } else if (
88
- head.provenance_source !== "portable_skill" ||
89
- head.scope !== "workspace" ||
90
- head.scope_workspace_id !== input.workspaceId
91
- ) {
132
+ } else if (impact.disposition === "retained") {
92
133
  receipts.push({
93
134
  skillId: head.id,
94
135
  revisionId: head.active_revision_id,
@@ -103,7 +144,7 @@ export async function releaseOrphanedSkillHeads(
103
144
  actorSubjectId: actor.subjectId,
104
145
  principalKind: actor.principalKind,
105
146
  preferenceId: head.id,
106
- expectedCurrentRevisionId: head.active_revision_id,
147
+ expectedCurrentRevisionId: head.active_revision_id!,
107
148
  expectedScopeVersion: head.scope_version,
108
149
  authorizeScope: (scope) => {
109
150
  if (scope !== "workspace")
package/src/skills.ts CHANGED
@@ -88,12 +88,13 @@ export async function applySkillLifecycle(
88
88
  );
89
89
  }
90
90
  }
91
- if (content === undefined) throw new Error("Skill lifecycle requires a readable SKILL.md");
92
- const metadata = readSkillMetadata(content);
91
+ const removing = request.operation === "remove" || Boolean(request.removalOperationId);
92
+ if (content === undefined && !removing)
93
+ throw new Error("Skill lifecycle requires a readable SKILL.md");
94
+ const metadata = removing ? null : readSkillMetadata(content!);
93
95
  const canonicalRequest = {
94
96
  ...request,
95
- title: metadata.name,
96
- description: metadata.description,
97
+ ...(metadata ? { title: metadata.name, description: metadata.description } : {}),
97
98
  };
98
99
  const rows = await rawRows<{ receipt: SkillWriteReceipt }>(
99
100
  tx,
@@ -117,6 +118,30 @@ export type SkillReadContext = {
117
118
  subjectId?: string;
118
119
  };
119
120
 
121
+ /** Scope only, for authorizing an exact human deletion retry after the head is gone.
122
+ * The lifecycle still verifies the complete request fingerprint before replay.
123
+ */
124
+ export async function readSkillRemovalScope(
125
+ db: Database,
126
+ context: SkillReadContext & { subjectId: string },
127
+ skillId: string,
128
+ operationId: string,
129
+ ): Promise<"workspace" | "user" | null> {
130
+ return withWorkspaceSubjectRls(db, context.workspaceId, context.subjectId, async (tx) => {
131
+ const [row] = await rawRows<{ scope: string }>(
132
+ tx,
133
+ sql`
134
+ SELECT receipt->>'removedScope' AS scope FROM skill_write_receipts
135
+ WHERE account_id=${context.accountId}::uuid AND workspace_id=${context.workspaceId}::uuid
136
+ AND operation_id=${operationId}::uuid AND receipt->>'skillId'=${skillId}
137
+ AND receipt->>'removed'='true' AND actor->>'kind'='human'
138
+ AND actor->>'principalKind'='human_session' AND actor->>'subjectId'=${context.subjectId}
139
+ `,
140
+ );
141
+ return row?.scope === "workspace" || row?.scope === "user" ? row.scope : null;
142
+ });
143
+ }
144
+
120
145
  /** Recheck the host-bound attempt before any agent-facing Skill read/search. */
121
146
  export async function assertSkillReadAttempt(
122
147
  db: Database,
@@ -212,6 +237,7 @@ export async function listSkillRecords(
212
237
  AND active.account_id=h.account_id AND active.expires_at <= transaction_timestamp()
213
238
  ) THEN 'expired' ELSE h.status END,
214
239
  'activeRevisionId',h.active_revision_id,'revisionId',r.id,
240
+ 'removalOperationId',r.skill_removal_operation_id,
215
241
  'activationMode',coalesce(r.skill_activation_mode,'workspace_managed'),
216
242
  'pendingRevisionIds',coalesce((SELECT jsonb_agg(DISTINCT pending.receipt->>'revisionId')
217
243
  FROM skill_write_receipts pending WHERE pending.account_id=h.account_id
@@ -346,7 +372,7 @@ export async function skillReviewResolution(
346
372
  db: Database,
347
373
  context: SkillReadContext,
348
374
  review: SkillReviewReference,
349
- ): Promise<"pending" | "activated" | "declined" | "superseded" | "unavailable"> {
375
+ ): Promise<"pending" | "activated" | "removed" | "declined" | "superseded" | "unavailable"> {
350
376
  const run = async (tx: Database) => {
351
377
  const [record] = await listSkillRecords(tx, context, {
352
378
  skillId: review.skillId,
@@ -354,7 +380,19 @@ export async function skillReviewResolution(
354
380
  metadataOnly: true,
355
381
  limit: 1,
356
382
  });
357
- if (!record) return "unavailable" as const;
383
+ if (!record) {
384
+ if (review.removalOperationId) {
385
+ const [removed] = await rawRows<{ operation_id: string }>(
386
+ tx,
387
+ sql`
388
+ SELECT operation_id FROM skill_write_receipts
389
+ WHERE account_id=${context.accountId}::uuid AND workspace_id=${context.workspaceId}::uuid
390
+ AND receipt->>'skillId'=${review.skillId} AND receipt->>'removed'='true' LIMIT 1`,
391
+ );
392
+ if (removed) return "removed" as const;
393
+ }
394
+ return "unavailable" as const;
395
+ }
358
396
  const [event] = await rawRows<{ type: string }>(
359
397
  tx,
360
398
  sql`
@@ -301,62 +301,6 @@ export async function issueSelfUserResourceGrant(
301
301
  });
302
302
  }
303
303
 
304
- export async function issueSelfLocalConnectionUseGrant(
305
- db: Database,
306
- input: {
307
- accountId: string;
308
- workspaceId: string;
309
- subjectId: string;
310
- authorityId: string;
311
- context: "user_private" | "workspace_shared";
312
- workspaceSharedAcknowledged: boolean;
313
- },
314
- ): Promise<UserResourceAuthorityGrant> {
315
- return await withOwnerContext(db, input, async (scopedDb) => {
316
- const [row] = await rawRows<
317
- Omit<UserResourceAuthorityGrant, "delegation"> & {
318
- organizationId: string;
319
- authorityGeneration: number;
320
- }
321
- >(
322
- scopedDb,
323
- sql`
324
- select grant_id as "grantId", organization_id as "organizationId",
325
- authority_generation::int as "authorityGeneration",
326
- target_workspace_id as "targetWorkspaceId",
327
- target_session_id as "targetSessionId", action, grant_mode as mode,
328
- grant_context as context, authority_epoch::int as "authorityEpoch",
329
- grant_generation::int as generation, grant_status as status,
330
- expires_at::text as "expiresAt"
331
- from issue_self_local_connection_use_grant(
332
- ${input.accountId}::uuid, ${input.authorityId}::uuid,
333
- ${input.workspaceId}::uuid, ${input.context},
334
- ${input.workspaceSharedAcknowledged}
335
- )
336
- `,
337
- );
338
- if (!row) throw new Error("local connection grant was not returned");
339
- const { organizationId, authorityGeneration, ...grant } = row;
340
- return {
341
- ...grant,
342
- expiresAt: rfc3339(grant.expiresAt),
343
- delegation: {
344
- authorityId: input.authorityId,
345
- grantId: grant.grantId,
346
- organizationId,
347
- workspaceId: grant.targetWorkspaceId,
348
- sessionId: grant.targetSessionId,
349
- action: grant.action,
350
- mode: grant.mode,
351
- context: grant.context,
352
- authorityEpoch: grant.authorityEpoch,
353
- authorityGeneration,
354
- grantGeneration: grant.generation,
355
- },
356
- };
357
- });
358
- }
359
-
360
304
  export async function revokeSelfUserResourceGrant(
361
305
  db: Database,
362
306
  input: { accountId: string; workspaceId: string; subjectId: string; grantId: string },