create-windy 0.6.14 → 0.6.15

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 (160) hide show
  1. package/dist/cli.js +215 -24
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +58 -8
  4. package/template/.windy-template.json +2 -2
  5. package/template/apps/server/src/agent/client-session/attempt-authority.test.ts +162 -0
  6. package/template/apps/server/src/agent/client-session/attempt-authority.ts +191 -0
  7. package/template/apps/server/src/agent/client-session/bootstrap.ts +27 -11
  8. package/template/apps/server/src/agent/client-session/completion.ts +75 -24
  9. package/template/apps/server/src/agent/client-session/contracts.ts +23 -0
  10. package/template/apps/server/src/agent/client-session/creation.ts +103 -0
  11. package/template/apps/server/src/agent/client-session/digest.ts +11 -1
  12. package/template/apps/server/src/agent/client-session/drizzle-records.ts +150 -0
  13. package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts +30 -3
  14. package/template/apps/server/src/agent/client-session/drizzle-repository.ts +23 -132
  15. package/template/apps/server/src/agent/client-session/finalization.ts +99 -0
  16. package/template/apps/server/src/agent/client-session/host-auth.ts +22 -10
  17. package/template/apps/server/src/agent/client-session/host-revocation-composition.test.ts +77 -0
  18. package/template/apps/server/src/agent/client-session/host-revocation-composition.ts +28 -0
  19. package/template/apps/server/src/agent/client-session/launch-authority.contract.test.ts +21 -0
  20. package/template/apps/server/src/agent/client-session/manifest-admission.ts +11 -0
  21. package/template/apps/server/src/agent/client-session/service.test-support.ts +22 -0
  22. package/template/apps/server/src/agent/client-session/service.ts +104 -134
  23. package/template/apps/server/src/agent/client-session/terminal-digest.test.ts +23 -0
  24. package/template/apps/server/src/agent/client-workflow-artifact/active-blob-references.ts +8 -0
  25. package/template/apps/server/src/agent/client-workflow-artifact/artifact-builder.ts +60 -0
  26. package/template/apps/server/src/agent/client-workflow-artifact/authority-current.ts +59 -0
  27. package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.test.ts +189 -0
  28. package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.ts +153 -0
  29. package/template/apps/server/src/agent/client-workflow-artifact/bootstrap.ts +77 -0
  30. package/template/apps/server/src/agent/client-workflow-artifact/cancellation.ts +47 -0
  31. package/template/apps/server/src/agent/client-workflow-artifact/completed-recovery.ts +31 -0
  32. package/template/apps/server/src/agent/client-workflow-artifact/contracts.ts +104 -0
  33. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-locks.ts +30 -0
  34. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-mapper.ts +138 -0
  35. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-repository.ts +346 -0
  36. package/template/apps/server/src/agent/client-workflow-artifact/error.ts +21 -0
  37. package/template/apps/server/src/agent/client-workflow-artifact/handler.ts +167 -0
  38. package/template/apps/server/src/agent/client-workflow-artifact/host-revocation.ts +29 -0
  39. package/template/apps/server/src/agent/client-workflow-artifact/in-memory-repository.ts +232 -0
  40. package/template/apps/server/src/agent/client-workflow-artifact/job-port.ts +101 -0
  41. package/template/apps/server/src/agent/client-workflow-artifact/presentation.ts +41 -0
  42. package/template/apps/server/src/agent/client-workflow-artifact/recovery.contract.test.ts +253 -0
  43. package/template/apps/server/src/agent/client-workflow-artifact/release-binding.ts +85 -0
  44. package/template/apps/server/src/agent/client-workflow-artifact/repository.test.ts +204 -0
  45. package/template/apps/server/src/agent/client-workflow-artifact/repository.ts +97 -0
  46. package/template/apps/server/src/agent/client-workflow-artifact/routes-handler.contract.test.ts +201 -0
  47. package/template/apps/server/src/agent/client-workflow-artifact/routes.ts +161 -0
  48. package/template/apps/server/src/agent/client-workflow-artifact/runtime-composition.ts +101 -0
  49. package/template/apps/server/src/agent/client-workflow-artifact/scope-authority.ts +39 -0
  50. package/template/apps/server/src/agent/client-workflow-artifact/service-authorization.ts +117 -0
  51. package/template/apps/server/src/agent/client-workflow-artifact/service-support.ts +28 -0
  52. package/template/apps/server/src/agent/client-workflow-artifact/service.contract.test.ts +345 -0
  53. package/template/apps/server/src/agent/client-workflow-artifact/service.ts +350 -0
  54. package/template/apps/server/src/agent/client-workflow-artifact/session-boundary.ts +39 -0
  55. package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts +98 -0
  56. package/template/apps/server/src/agent/client-workflow-artifact/source-access.ts +80 -0
  57. package/template/apps/server/src/agent/client-workflow-artifact/takeover.ts +122 -0
  58. package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts +97 -0
  59. package/template/apps/server/src/agent/client-workflow-artifact/task-authority.ts +147 -0
  60. package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts +100 -0
  61. package/template/apps/server/src/agent/client-workflow-artifact/terminal-replay.ts +31 -0
  62. package/template/apps/server/src/agent/client-workflow-artifact/terminal-seal.ts +211 -0
  63. package/template/apps/server/src/agent/client-workflow-artifact/workflow-artifact.test-support.ts +173 -0
  64. package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.contract.test.ts +254 -0
  65. package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.ts +285 -0
  66. package/template/apps/server/src/agent/run-facade.test-support.ts +17 -4
  67. package/template/apps/server/src/agent/run-facade.test.ts +16 -0
  68. package/template/apps/server/src/agent/run-session-authorization.ts +12 -10
  69. package/template/apps/server/src/agent/runtime-bootstrap-input.ts +5 -1
  70. package/template/apps/server/src/agent/runtime-bootstrap.ts +33 -33
  71. package/template/apps/server/src/agent/runtime-public-ports.ts +40 -0
  72. package/template/apps/server/src/agent/session-invalidation/contracts.ts +25 -26
  73. package/template/apps/server/src/agent/session-projection-access.ts +31 -19
  74. package/template/apps/server/src/agent/unavailable-routes.ts +21 -0
  75. package/template/apps/server/src/agent/workflow-runtime-bootstrap.ts +2 -0
  76. package/template/apps/server/src/agent-workflow/artifact-source-authorization.ts +12 -6
  77. package/template/apps/server/src/agent-workflow/command-facade.ts +70 -11
  78. package/template/apps/server/src/agent-workflow/drizzle-artifact-port.ts +9 -9
  79. package/template/apps/server/src/agent-workflow/drizzle-delegation-completion.ts +1 -1
  80. package/template/apps/server/src/agent-workflow/platform-node-adapters.ts +14 -6
  81. package/template/apps/server/src/agent-workflow/project-launch-authority.contract.test.ts +53 -0
  82. package/template/apps/server/src/agent-workflow/project-launch-authority.ts +92 -0
  83. package/template/apps/server/src/agent-workflow/runtime-composition.ts +30 -14
  84. package/template/apps/server/src/app/agent-runtime-composition.ts +6 -1
  85. package/template/apps/server/src/app/client-agent-workflow-artifact-binding.ts +14 -0
  86. package/template/apps/server/src/application-services.ts +6 -0
  87. package/template/apps/server/src/data-access/scope-containment.test.ts +31 -0
  88. package/template/apps/server/src/data-access/scope-containment.ts +48 -0
  89. package/template/apps/server/src/jobs/artifact-publication.test.ts +192 -0
  90. package/template/apps/server/src/jobs/artifact-publication.ts +56 -0
  91. package/template/apps/server/src/jobs/artifact-publisher.ts +101 -0
  92. package/template/apps/server/src/jobs/drizzle-artifact.ts +73 -1
  93. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +14 -1
  94. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +65 -0
  95. package/template/apps/server/src/jobs/drizzle-repository.integration.test.ts +1 -0
  96. package/template/apps/server/src/jobs/execution-repository.ts +28 -0
  97. package/template/apps/server/src/jobs/execution.test.ts +3 -1
  98. package/template/apps/server/src/jobs/in-memory-execution.ts +16 -0
  99. package/template/apps/server/src/jobs/in-memory-lease.ts +45 -1
  100. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +2 -0
  101. package/template/apps/server/src/jobs/postgres-cleanup-contract.test.ts +58 -0
  102. package/template/apps/server/src/jobs/worker.ts +8 -14
  103. package/template/apps/server/src/module-host/background-ai-unavailable.ts +18 -0
  104. package/template/apps/server/src/module-host/durable-context.ts +19 -22
  105. package/template/apps/server/src/module-host/implementation-completeness.test.ts +80 -1
  106. package/template/apps/server/src/module-host/implementation-completeness.ts +28 -1
  107. package/template/apps/server/src/module-host/request-context.ts +32 -8
  108. package/template/apps/server/src/module-host/storage-port.ts +3 -0
  109. package/template/apps/server/src/module-host/workflow-input-artifact-port.test.ts +104 -0
  110. package/template/apps/server/src/module-host/workflow-input-artifact-port.ts +229 -0
  111. package/template/apps/server/src/module-host/workflow-input-materialization.test.ts +163 -0
  112. package/template/apps/server/src/module-host/workflow-input-materialization.ts +161 -0
  113. package/template/apps/server/src/workflow/drizzle-repository.integration.test.ts +1 -0
  114. package/template/docs/architecture/agent-workflow.md +13 -2
  115. package/template/docs/architecture/durable-jobs.md +7 -0
  116. package/template/docs/operations/agent-session-memory.md +2 -1
  117. package/template/docs/operations/agent-session-troubleshooting.md +87 -2
  118. package/template/docs/operations/agent-workflow-recovery.md +25 -14
  119. package/template/docs/platform/client-agent-session.md +148 -2
  120. package/template/packages/ai-client/package.json +2 -1
  121. package/template/packages/ai-client/src/client-agent/index.ts +1 -0
  122. package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
  123. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
  124. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
  125. package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
  126. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
  127. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
  128. package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
  129. package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
  130. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  131. package/template/packages/database/package.json +1 -1
  132. package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
  133. package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
  134. package/template/packages/database/src/schema/index.ts +1 -0
  135. package/template/packages/jobs/package.json +1 -1
  136. package/template/packages/jobs/src/handlers.test.ts +10 -0
  137. package/template/packages/jobs/src/handlers.ts +28 -11
  138. package/template/packages/modules/package.json +1 -1
  139. package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
  140. package/template/packages/modules/src/manifest.ts +2 -0
  141. package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
  142. package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
  143. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  144. package/template/packages/server-sdk/index.ts +52 -0
  145. package/template/packages/server-sdk/package.json +5 -1
  146. package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
  147. package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
  148. package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
  149. package/template/packages/server-sdk/src/durable-handler.ts +7 -0
  150. package/template/packages/server-sdk/src/request-context.ts +7 -0
  151. package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
  152. package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
  153. package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
  154. package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
  155. package/template/packages/storage/package.json +1 -1
  156. package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
  157. package/template/packages/storage/src/blob-store.ts +9 -0
  158. package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
  159. package/template/packages/storage/src/local-file-blob-store.ts +53 -1
  160. package/template/packages/storage/src/memory-blob-store.ts +73 -33
@@ -0,0 +1,138 @@
1
+ import { clientAgentWorkflowArtifacts } from "@southwind-ai/database";
2
+ import type { ArtifactRef } from "@southwind-ai/storage";
3
+ import type { ClientAgentWorkflowArtifactRecord } from "./contracts.js";
4
+
5
+ type Row = typeof clientAgentWorkflowArtifacts.$inferSelect;
6
+
7
+ export function clientWorkflowArtifactRow(
8
+ value: ClientAgentWorkflowArtifactRecord,
9
+ ): typeof clientAgentWorkflowArtifacts.$inferInsert {
10
+ return {
11
+ workId: value.workId,
12
+ commandId: value.commandId,
13
+ durableJobId: value.durableJobId,
14
+ runId: value.runId,
15
+ nodeRunId: value.nodeRunId,
16
+ effectKind: value.effectKind,
17
+ taskKey: value.taskKey,
18
+ operationKey: value.operationKey,
19
+ bindingDigest: value.bindingDigest,
20
+ actorId: value.actorId,
21
+ scopeDigest: value.scopeDigest,
22
+ authorizationSnapshotDigest: value.authorizationSnapshotDigest,
23
+ runPolicyDigest: value.runPolicyDigest,
24
+ requestDigest: value.requestDigest,
25
+ inputDigest: value.inputDigest,
26
+ effectGeneration: value.effectGeneration,
27
+ effectFenceTokenDigest: value.effectFenceTokenDigest,
28
+ status: value.status,
29
+ claimGeneration: value.claimGeneration,
30
+ clientBindingDigest: value.clientBindingDigest ?? null,
31
+ installationId: value.installationId ?? null,
32
+ hostSessionDigest: value.hostSessionDigest ?? null,
33
+ clientSessionId: value.clientSessionId ?? null,
34
+ claimTokenHash: value.claimTokenHash ?? null,
35
+ claimExpiresAt: value.claimExpiresAt ?? null,
36
+ historySequence: value.historySequence,
37
+ historyHeadDigest: value.historyHeadDigest ?? null,
38
+ terminalReceiptId: value.terminalReceiptId ?? null,
39
+ terminalReceiptDigest: value.terminalReceiptDigest ?? null,
40
+ terminalRequestDigest: value.terminalRequestDigest ?? null,
41
+ terminalResultJson: value.terminalResult ?? null,
42
+ artifactRef: value.artifact?.artifactRef ?? null,
43
+ artifactFilename: value.artifact?.filename ?? null,
44
+ artifactContentType: value.artifact?.contentType ?? null,
45
+ artifactByteSize: value.artifact?.byteSize ?? null,
46
+ artifactSha256: value.artifact?.sha256 ?? null,
47
+ cancelRequestedAt: value.cancelRequestedAt ?? null,
48
+ finishedAt: value.finishedAt ?? null,
49
+ version: value.version,
50
+ createdAt: value.createdAt,
51
+ updatedAt: value.updatedAt,
52
+ };
53
+ }
54
+
55
+ export function toClientWorkflowArtifact(
56
+ row: Row,
57
+ ): ClientAgentWorkflowArtifactRecord {
58
+ const artifact =
59
+ row.artifactRef &&
60
+ row.artifactFilename &&
61
+ row.artifactContentType &&
62
+ row.artifactByteSize !== null &&
63
+ row.artifactSha256
64
+ ? {
65
+ artifactRef: row.artifactRef as ArtifactRef,
66
+ filename: row.artifactFilename,
67
+ contentType: row.artifactContentType,
68
+ byteSize: row.artifactByteSize,
69
+ sha256: row.artifactSha256,
70
+ }
71
+ : undefined;
72
+ return {
73
+ workId: row.workId,
74
+ commandId: row.commandId,
75
+ durableJobId: row.durableJobId,
76
+ runId: row.runId,
77
+ nodeRunId: row.nodeRunId,
78
+ effectKind: row.effectKind as "job",
79
+ taskKey: row.taskKey as ClientAgentWorkflowArtifactRecord["taskKey"],
80
+ operationKey: row.operationKey,
81
+ bindingDigest: row.bindingDigest as `sha256:${string}`,
82
+ actorId: row.actorId,
83
+ scopeDigest: row.scopeDigest as `sha256:${string}`,
84
+ authorizationSnapshotDigest:
85
+ row.authorizationSnapshotDigest as `sha256:${string}`,
86
+ runPolicyDigest: row.runPolicyDigest as `sha256:${string}`,
87
+ requestDigest: row.requestDigest as `sha256:${string}`,
88
+ inputDigest: row.inputDigest as `sha256:${string}`,
89
+ effectGeneration: row.effectGeneration,
90
+ effectFenceTokenDigest: row.effectFenceTokenDigest as `sha256:${string}`,
91
+ status: row.status,
92
+ claimGeneration: row.claimGeneration,
93
+ ...(row.clientBindingDigest
94
+ ? {
95
+ clientBindingDigest: row.clientBindingDigest as `sha256:${string}`,
96
+ }
97
+ : {}),
98
+ ...(row.installationId ? { installationId: row.installationId } : {}),
99
+ ...(row.hostSessionDigest
100
+ ? { hostSessionDigest: row.hostSessionDigest as `sha256:${string}` }
101
+ : {}),
102
+ ...(row.clientSessionId ? { clientSessionId: row.clientSessionId } : {}),
103
+ ...(row.claimTokenHash
104
+ ? { claimTokenHash: row.claimTokenHash as `sha256:${string}` }
105
+ : {}),
106
+ ...(row.claimExpiresAt ? { claimExpiresAt: row.claimExpiresAt } : {}),
107
+ historySequence: row.historySequence,
108
+ ...(row.historyHeadDigest
109
+ ? { historyHeadDigest: row.historyHeadDigest as `sha256:${string}` }
110
+ : {}),
111
+ ...(row.terminalReceiptId
112
+ ? { terminalReceiptId: row.terminalReceiptId }
113
+ : {}),
114
+ ...(row.terminalReceiptDigest
115
+ ? {
116
+ terminalReceiptDigest:
117
+ row.terminalReceiptDigest as `sha256:${string}`,
118
+ }
119
+ : {}),
120
+ ...(row.terminalRequestDigest
121
+ ? {
122
+ terminalRequestDigest:
123
+ row.terminalRequestDigest as `sha256:${string}`,
124
+ }
125
+ : {}),
126
+ ...(row.terminalResultJson
127
+ ? { terminalResult: row.terminalResultJson }
128
+ : {}),
129
+ ...(artifact ? { artifact } : {}),
130
+ ...(row.cancelRequestedAt
131
+ ? { cancelRequestedAt: row.cancelRequestedAt }
132
+ : {}),
133
+ ...(row.finishedAt ? { finishedAt: row.finishedAt } : {}),
134
+ version: row.version,
135
+ createdAt: row.createdAt,
136
+ updatedAt: row.updatedAt,
137
+ };
138
+ }
@@ -0,0 +1,346 @@
1
+ import { clientAgentWorkflowArtifacts } from "@southwind-ai/database";
2
+ import { and, desc, eq, inArray, sql } from "drizzle-orm";
3
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+ import type { ArtifactRef } from "@southwind-ai/storage";
5
+ import type {
6
+ ClientAgentWorkflowArtifactRecord,
7
+ ClientAgentWorkflowClaimBinding,
8
+ } from "./contracts.js";
9
+ import {
10
+ clientWorkflowArtifactRow,
11
+ toClientWorkflowArtifact,
12
+ } from "./drizzle-mapper.js";
13
+ import {
14
+ type ClientAgentWorkflowArtifactRepository,
15
+ sameWorkflowArtifactIdentity,
16
+ } from "./repository.js";
17
+ import { clientWorkflowAuthorityCurrent } from "./authority-current.js";
18
+ import {
19
+ lockClientWorkflowArtifact as locked,
20
+ lockClientWorkflowArtifactBySession as lockedByClientSession,
21
+ } from "./drizzle-locks.js";
22
+
23
+ export class DrizzleClientAgentWorkflowArtifactRepository implements ClientAgentWorkflowArtifactRepository {
24
+ constructor(private readonly db: NodePgDatabase) {}
25
+
26
+ begin(record: ClientAgentWorkflowArtifactRecord) {
27
+ return this.db.transaction(async (tx) => {
28
+ const replay = await locked(tx, record.workId);
29
+ if (!(await clientWorkflowAuthorityCurrent(tx, record, Date.now()))) {
30
+ throw new Error("CLIENT_AGENT_WORKFLOW_AUTHORITY_STALE");
31
+ }
32
+ if (replay) {
33
+ if (!sameWorkflowArtifactIdentity(replay, record)) {
34
+ throw new Error("CLIENT_AGENT_WORKFLOW_IDENTITY_CONFLICT");
35
+ }
36
+ return replay;
37
+ }
38
+ await tx
39
+ .insert(clientAgentWorkflowArtifacts)
40
+ .values(clientWorkflowArtifactRow(record))
41
+ .onConflictDoNothing({
42
+ target: clientAgentWorkflowArtifacts.commandId,
43
+ });
44
+ const existing = await locked(tx, record.workId);
45
+ if (!existing || !sameWorkflowArtifactIdentity(existing, record)) {
46
+ throw new Error("CLIENT_AGENT_WORKFLOW_IDENTITY_CONFLICT");
47
+ }
48
+ return existing;
49
+ });
50
+ }
51
+
52
+ find(workId: string) {
53
+ return this.findWhere(eq(clientAgentWorkflowArtifacts.workId, workId));
54
+ }
55
+
56
+ findByJob(durableJobId: string) {
57
+ return this.findWhere(
58
+ eq(clientAgentWorkflowArtifacts.durableJobId, durableJobId),
59
+ );
60
+ }
61
+
62
+ authorizeClientSessionAttempt(
63
+ input: Parameters<
64
+ ClientAgentWorkflowArtifactRepository["authorizeClientSessionAttempt"]
65
+ >[0],
66
+ ) {
67
+ return this.db.transaction((tx) =>
68
+ this.authorizeClientSessionAttemptInTransaction(
69
+ tx as unknown as NodePgDatabase,
70
+ input,
71
+ ),
72
+ );
73
+ }
74
+
75
+ async authorizeClientSessionAttemptInTransaction(
76
+ transaction: NodePgDatabase,
77
+ input: Parameters<
78
+ ClientAgentWorkflowArtifactRepository["authorizeClientSessionAttempt"]
79
+ >[0],
80
+ ) {
81
+ const current = await lockedByClientSession(
82
+ transaction,
83
+ input.clientSessionId,
84
+ );
85
+ if (!current) return "ordinary" as const;
86
+ if (
87
+ current.status !== "claimed" ||
88
+ !current.claimExpiresAt ||
89
+ current.claimExpiresAt <= input.now ||
90
+ current.actorId !== input.actorId ||
91
+ current.operationKey !== input.operationKey ||
92
+ current.clientBindingDigest !== input.clientBindingDigest ||
93
+ current.hostSessionDigest !== input.hostSessionDigest ||
94
+ !(await clientWorkflowAuthorityCurrent(transaction, current, input.now))
95
+ ) {
96
+ return "revoked" as const;
97
+ }
98
+ return "current" as const;
99
+ }
100
+
101
+ findForActorRun(input: { actorId: string; runId: string }) {
102
+ return this.findWhere(
103
+ and(
104
+ eq(clientAgentWorkflowArtifacts.actorId, input.actorId),
105
+ eq(clientAgentWorkflowArtifacts.runId, input.runId),
106
+ )!,
107
+ );
108
+ }
109
+
110
+ claim(input: {
111
+ workId: string;
112
+ expectedVersion: number;
113
+ allowActiveTakeover?: boolean;
114
+ binding: ClientAgentWorkflowClaimBinding;
115
+ }) {
116
+ return this.db.transaction(async (tx) => {
117
+ const current = await locked(tx, input.workId);
118
+ if (!current || current.version !== input.expectedVersion) {
119
+ return { outcome: "conflict" as const };
120
+ }
121
+ if (current.status === "completed" || current.status === "cancelled") {
122
+ return { outcome: current.status, record: current } as const;
123
+ }
124
+ if (
125
+ !(await clientWorkflowAuthorityCurrent(tx, current, input.binding.now))
126
+ ) {
127
+ return { outcome: "conflict" as const };
128
+ }
129
+ if (
130
+ current.status !== "pending" &&
131
+ (current.status !== "claimed" || !input.allowActiveTakeover)
132
+ ) {
133
+ return { outcome: "busy" as const, record: current };
134
+ }
135
+ const previousClientSessionId = current.clientSessionId;
136
+ const [row] = await tx
137
+ .update(clientAgentWorkflowArtifacts)
138
+ .set({
139
+ status: "claimed",
140
+ claimGeneration: current.claimGeneration + 1,
141
+ clientBindingDigest: input.binding.clientBindingDigest,
142
+ installationId: input.binding.installationId,
143
+ hostSessionDigest: input.binding.hostSessionDigest,
144
+ clientSessionId: null,
145
+ claimTokenHash: input.binding.claimTokenHash,
146
+ claimExpiresAt: input.binding.claimExpiresAt,
147
+ updatedAt: input.binding.now,
148
+ version: current.version + 1,
149
+ })
150
+ .where(
151
+ and(
152
+ eq(clientAgentWorkflowArtifacts.workId, current.workId),
153
+ eq(clientAgentWorkflowArtifacts.version, current.version),
154
+ ),
155
+ )
156
+ .returning();
157
+ if (!row) return { outcome: "conflict" as const };
158
+ return {
159
+ outcome: "claimed" as const,
160
+ record: toClientWorkflowArtifact(row),
161
+ ...(previousClientSessionId ? { previousClientSessionId } : {}),
162
+ };
163
+ });
164
+ }
165
+
166
+ async bindClientSession(input: {
167
+ workId: string;
168
+ claimGeneration: number;
169
+ clientSessionId: string;
170
+ now: number;
171
+ }) {
172
+ const [row] = await this.db
173
+ .update(clientAgentWorkflowArtifacts)
174
+ .set({
175
+ clientSessionId: input.clientSessionId,
176
+ updatedAt: input.now,
177
+ version: sql`${clientAgentWorkflowArtifacts.version} + 1`,
178
+ })
179
+ .where(
180
+ and(
181
+ eq(clientAgentWorkflowArtifacts.workId, input.workId),
182
+ eq(clientAgentWorkflowArtifacts.status, "claimed"),
183
+ eq(
184
+ clientAgentWorkflowArtifacts.claimGeneration,
185
+ input.claimGeneration,
186
+ ),
187
+ sql`${clientAgentWorkflowArtifacts.clientSessionId} is null`,
188
+ ),
189
+ )
190
+ .returning();
191
+ return row ? toClientWorkflowArtifact(row) : undefined;
192
+ }
193
+
194
+ complete(
195
+ input: Parameters<ClientAgentWorkflowArtifactRepository["complete"]>[0],
196
+ ) {
197
+ return this.db.transaction(async (tx) => {
198
+ const current = await locked(tx, input.workId);
199
+ if (
200
+ !current ||
201
+ current.status !== "claimed" ||
202
+ current.claimGeneration !== input.terminal.claimGeneration ||
203
+ current.clientSessionId !== input.terminal.clientSessionId ||
204
+ current.claimTokenHash !== input.claimTokenHash
205
+ ) {
206
+ return undefined;
207
+ }
208
+ if (
209
+ !(await clientWorkflowAuthorityCurrent(tx, current, input.terminal.now))
210
+ ) {
211
+ return undefined;
212
+ }
213
+ const [row] = await tx
214
+ .update(clientAgentWorkflowArtifacts)
215
+ .set({
216
+ status: "completed",
217
+ historySequence: input.terminal.historySequence,
218
+ historyHeadDigest: input.terminal.historyHeadDigest,
219
+ terminalReceiptId: input.terminal.terminalReceiptId,
220
+ terminalReceiptDigest: input.terminal.terminalReceiptDigest,
221
+ terminalRequestDigest: input.terminal.terminalRequestDigest,
222
+ terminalResultJson: input.terminal.terminalResult,
223
+ artifactRef: input.terminal.artifact.artifactRef,
224
+ artifactFilename: input.terminal.artifact.filename,
225
+ artifactContentType: input.terminal.artifact.contentType,
226
+ artifactByteSize: input.terminal.artifact.byteSize,
227
+ artifactSha256: input.terminal.artifact.sha256,
228
+ finishedAt: input.terminal.now,
229
+ updatedAt: input.terminal.now,
230
+ version: current.version + 1,
231
+ })
232
+ .where(
233
+ and(
234
+ eq(clientAgentWorkflowArtifacts.workId, current.workId),
235
+ eq(clientAgentWorkflowArtifacts.version, current.version),
236
+ ),
237
+ )
238
+ .returning();
239
+ return row ? toClientWorkflowArtifact(row) : undefined;
240
+ });
241
+ }
242
+
243
+ async requestCancel(input: { workId: string; now: number }) {
244
+ return this.db.transaction(async (tx) => {
245
+ const current = await locked(tx, input.workId);
246
+ if (!current) return undefined;
247
+ if (current.status === "cancelled") return current;
248
+ if (current.status === "completed") return undefined;
249
+ if (current.status === "cancellation-requested") return current;
250
+ const [row] = await tx
251
+ .update(clientAgentWorkflowArtifacts)
252
+ .set({
253
+ status: "cancellation-requested",
254
+ cancelRequestedAt: input.now,
255
+ updatedAt: input.now,
256
+ version: current.version + 1,
257
+ })
258
+ .where(
259
+ and(
260
+ eq(clientAgentWorkflowArtifacts.workId, current.workId),
261
+ eq(clientAgentWorkflowArtifacts.version, current.version),
262
+ ),
263
+ )
264
+ .returning();
265
+ return row ? toClientWorkflowArtifact(row) : undefined;
266
+ });
267
+ }
268
+
269
+ async finalizeCancel(input: { workId: string; now: number }) {
270
+ return this.db.transaction(async (tx) => {
271
+ const current = await locked(tx, input.workId);
272
+ if (!current) return undefined;
273
+ if (current.status === "cancelled") return current;
274
+ if (current.status !== "cancellation-requested") return undefined;
275
+ const [row] = await tx
276
+ .update(clientAgentWorkflowArtifacts)
277
+ .set({
278
+ status: "cancelled",
279
+ finishedAt: input.now,
280
+ updatedAt: input.now,
281
+ version: current.version + 1,
282
+ })
283
+ .where(
284
+ and(
285
+ eq(clientAgentWorkflowArtifacts.workId, current.workId),
286
+ eq(clientAgentWorkflowArtifacts.version, current.version),
287
+ ),
288
+ )
289
+ .returning();
290
+ return row ? toClientWorkflowArtifact(row) : undefined;
291
+ });
292
+ }
293
+
294
+ async listClaimedByHost(input: {
295
+ actorId: string;
296
+ hostSessionDigest: `sha256:${string}`;
297
+ }) {
298
+ const rows = await this.db
299
+ .select()
300
+ .from(clientAgentWorkflowArtifacts)
301
+ .where(
302
+ and(
303
+ eq(clientAgentWorkflowArtifacts.actorId, input.actorId),
304
+ eq(
305
+ clientAgentWorkflowArtifacts.hostSessionDigest,
306
+ input.hostSessionDigest,
307
+ ),
308
+ inArray(clientAgentWorkflowArtifacts.status, [
309
+ "claimed",
310
+ "cancellation-requested",
311
+ ]),
312
+ ),
313
+ )
314
+ .orderBy(desc(clientAgentWorkflowArtifacts.updatedAt));
315
+ return rows.map(toClientWorkflowArtifact);
316
+ }
317
+
318
+ async listActiveArtifactRefs(_now: Date): Promise<ArtifactRef[]> {
319
+ const rows = await this.db
320
+ .select({ artifactRef: clientAgentWorkflowArtifacts.artifactRef })
321
+ .from(clientAgentWorkflowArtifacts)
322
+ .where(eq(clientAgentWorkflowArtifacts.status, "completed"));
323
+ return rows.flatMap(({ artifactRef: ref }) =>
324
+ ref ? [ref as ArtifactRef] : [],
325
+ );
326
+ }
327
+
328
+ async listCancellationRequested() {
329
+ const rows = await this.db
330
+ .select()
331
+ .from(clientAgentWorkflowArtifacts)
332
+ .where(eq(clientAgentWorkflowArtifacts.status, "cancellation-requested"))
333
+ .orderBy(desc(clientAgentWorkflowArtifacts.updatedAt));
334
+ return rows.map(toClientWorkflowArtifact);
335
+ }
336
+
337
+ private async findWhere(predicate: ReturnType<typeof eq>) {
338
+ const [row] = await this.db
339
+ .select()
340
+ .from(clientAgentWorkflowArtifacts)
341
+ .where(predicate)
342
+ .orderBy(desc(clientAgentWorkflowArtifacts.createdAt))
343
+ .limit(1);
344
+ return row ? toClientWorkflowArtifact(row) : undefined;
345
+ }
346
+ }
@@ -0,0 +1,21 @@
1
+ export type ClientAgentWorkflowArtifactErrorCode =
2
+ | "NOT_FOUND"
3
+ | "INVALID_INPUT"
4
+ | "CONFLICT"
5
+ | "FORBIDDEN"
6
+ | "CANCELLED"
7
+ | "RECEIPT_REJECTED"
8
+ | "ARTIFACT_TOO_LARGE"
9
+ | "ARTIFACT_STORE_UNAVAILABLE"
10
+ | "PROVIDER_FAILED";
11
+
12
+ export class ClientAgentWorkflowArtifactError extends Error {
13
+ constructor(
14
+ readonly code: ClientAgentWorkflowArtifactErrorCode,
15
+ message: string,
16
+ readonly status: number,
17
+ ) {
18
+ super(message);
19
+ this.name = "ClientAgentWorkflowArtifactError";
20
+ }
21
+ }
@@ -0,0 +1,167 @@
1
+ import {
2
+ DurableHandlerError,
3
+ type DurableHandlerContext,
4
+ type DurableHandlerRegistry,
5
+ } from "@southwind-ai/jobs";
6
+ import { CLIENT_AGENT_WORKFLOW_ARTIFACT_TASK_KEY } from "@southwind-ai/server-sdk";
7
+ import {
8
+ clientAgentWorkflowArtifactSourceKind,
9
+ clientAgentWorkflowArtifactSourceModule,
10
+ type ClientAgentWorkflowArtifactBinding,
11
+ type ClientAgentWorkflowArtifactRecord,
12
+ } from "./contracts.js";
13
+ import type { GovernedClientAgentWorkflowArtifactService } from "./service.js";
14
+ import type { ClientAgentWorkflowJobPayload } from "./workflow-loader.js";
15
+ import { GovernedClientAgentWorkflowArtifactJobPort } from "./job-port.js";
16
+
17
+ export function registerClientAgentWorkflowArtifactHandler(input: {
18
+ registry: DurableHandlerRegistry;
19
+ service: GovernedClientAgentWorkflowArtifactService;
20
+ }) {
21
+ input.registry.register({
22
+ key: CLIENT_AGENT_WORKFLOW_ARTIFACT_TASK_KEY,
23
+ payload: { parse: parsePayload },
24
+ execute: (payload, context) => execute(input.service, payload, context),
25
+ });
26
+ }
27
+
28
+ async function execute(
29
+ service: GovernedClientAgentWorkflowArtifactService,
30
+ payload: ClientAgentWorkflowJobPayload,
31
+ context: DurableHandlerContext,
32
+ ) {
33
+ const begun = await service.begin({
34
+ jobId: context.jobId,
35
+ payload,
36
+ signal: context.signal,
37
+ });
38
+ const port = new GovernedClientAgentWorkflowArtifactJobPort(
39
+ service,
40
+ context.jobId,
41
+ begun.binding,
42
+ begun.record,
43
+ context.signal,
44
+ );
45
+ let view = await port.begin({ input: begun.input });
46
+ try {
47
+ while (!["completed", "cancelled"].includes(view.status)) {
48
+ await waitForChange(context.signal);
49
+ view = await port.claimOrAdvance(view.command);
50
+ }
51
+ } catch (error) {
52
+ await port.reconcileOrCancel(view.command);
53
+ throw error;
54
+ }
55
+ if (view.status === "cancelled") {
56
+ throw new DurableHandlerError(
57
+ "CLIENT_AGENT_WORKFLOW_CANCELLED",
58
+ "Client Agent Workflow 已取消",
59
+ false,
60
+ );
61
+ }
62
+ await publishStableArtifact(port.record, port.binding, context);
63
+ }
64
+
65
+ async function publishStableArtifact(
66
+ record: ClientAgentWorkflowArtifactRecord,
67
+ binding: ClientAgentWorkflowArtifactBinding,
68
+ context: DurableHandlerContext,
69
+ ) {
70
+ if (
71
+ !record.artifact ||
72
+ !record.terminalReceiptId ||
73
+ !record.terminalReceiptDigest ||
74
+ !record.historyHeadDigest ||
75
+ record.historySequence < 1
76
+ ) {
77
+ throw new DurableHandlerError(
78
+ "CLIENT_AGENT_WORKFLOW_RECEIPT_INVALID",
79
+ "Client Agent Workflow 终态回执不完整",
80
+ false,
81
+ );
82
+ }
83
+ if (!context.publishArtifactOnce) {
84
+ throw new DurableHandlerError(
85
+ "STABLE_ARTIFACT_PUBLICATION_UNAVAILABLE",
86
+ "稳定产物发布能力未装配",
87
+ false,
88
+ );
89
+ }
90
+ await context.publishArtifactOnce(record.terminalReceiptId, {
91
+ kind: binding.artifact.kind,
92
+ artifactRef: record.artifact.artifactRef,
93
+ filename: record.artifact.filename,
94
+ contentType: record.artifact.contentType,
95
+ byteSize: record.artifact.byteSize,
96
+ sha256: record.artifact.sha256,
97
+ ownerId: record.actorId,
98
+ metadata: {
99
+ retentionPolicyKey: binding.artifact.retentionPolicyKey,
100
+ sourceModuleKey: clientAgentWorkflowArtifactSourceModule,
101
+ sourceKind: clientAgentWorkflowArtifactSourceKind,
102
+ sourceId: record.workId,
103
+ sourceVersion: record.terminalReceiptId,
104
+ sourceSequence: record.historySequence,
105
+ sourceDigest: record.terminalReceiptDigest,
106
+ workflowClaimGeneration: record.claimGeneration,
107
+ historyHeadDigest: record.historyHeadDigest,
108
+ },
109
+ });
110
+ }
111
+
112
+ function parsePayload(value: unknown): ClientAgentWorkflowJobPayload {
113
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
114
+ throw invalidPayload();
115
+ }
116
+ const input = value as Record<string, unknown>;
117
+ const allowed = new Set([
118
+ "commandId",
119
+ "runId",
120
+ "nodeRunId",
121
+ "effectGeneration",
122
+ "requestDigest",
123
+ ]);
124
+ if (Object.keys(input).some((key) => !allowed.has(key))) {
125
+ throw invalidPayload();
126
+ }
127
+ const text = (key: string) => {
128
+ const current = input[key];
129
+ if (typeof current !== "string" || !current.trim()) throw invalidPayload();
130
+ return current;
131
+ };
132
+ const effectGeneration = input.effectGeneration;
133
+ if (!Number.isSafeInteger(effectGeneration) || Number(effectGeneration) < 1) {
134
+ throw invalidPayload();
135
+ }
136
+ return {
137
+ commandId: text("commandId"),
138
+ runId: text("runId"),
139
+ nodeRunId: text("nodeRunId"),
140
+ effectGeneration: Number(effectGeneration),
141
+ requestDigest: text("requestDigest"),
142
+ };
143
+ }
144
+
145
+ function waitForChange(signal: AbortSignal) {
146
+ return new Promise<void>((resolve, reject) => {
147
+ if (signal.aborted) return reject(signal.reason);
148
+ const timeout = setTimeout(done, 500);
149
+ signal.addEventListener("abort", aborted, { once: true });
150
+ function done() {
151
+ signal.removeEventListener("abort", aborted);
152
+ resolve();
153
+ }
154
+ function aborted() {
155
+ clearTimeout(timeout);
156
+ reject(signal.reason);
157
+ }
158
+ });
159
+ }
160
+
161
+ function invalidPayload() {
162
+ return new DurableHandlerError(
163
+ "CLIENT_AGENT_WORKFLOW_PAYLOAD_INVALID",
164
+ "Client Agent Workflow 任务载荷无效",
165
+ false,
166
+ );
167
+ }
@@ -0,0 +1,29 @@
1
+ import type { GovernedClientAgentSessionService } from "../client-session/service.js";
2
+ import type { ClientAgentWorkflowArtifactRecord } from "./contracts.js";
3
+ import type { ClientAgentWorkflowArtifactRepository } from "./repository.js";
4
+ import { cancelClientWorkflowArtifact } from "./cancellation.js";
5
+
6
+ export async function revokeClientWorkflowArtifactHost(input: {
7
+ records: readonly ClientAgentWorkflowArtifactRecord[];
8
+ repository: ClientAgentWorkflowArtifactRepository;
9
+ sessions: GovernedClientAgentSessionService;
10
+ now: () => number;
11
+ }) {
12
+ const errors: unknown[] = [];
13
+ for (const record of input.records) {
14
+ try {
15
+ await cancelClientWorkflowArtifact({
16
+ record,
17
+ repository: input.repository,
18
+ sessions: input.sessions,
19
+ cause: "workflow-host-revoked",
20
+ now: input.now(),
21
+ });
22
+ } catch (error) {
23
+ errors.push(error);
24
+ }
25
+ }
26
+ if (errors.length)
27
+ throw new AggregateError(errors, "Workflow Host 撤销未完全收敛");
28
+ return input.records.length;
29
+ }