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
@@ -14,6 +14,7 @@ import { artifactRef } from "@southwind-ai/storage";
14
14
  import { DrizzleDurableExecutionRepository } from "./drizzle-execution-repository.js";
15
15
  import { DrizzleDurableJobRepository } from "./drizzle-repository.js";
16
16
  import { DurableJobService } from "./service.js";
17
+ import { DurableArtifactPublicationConflictError } from "./execution-repository.js";
17
18
 
18
19
  const databaseUrl = process.env.JOBS_TEST_DATABASE_URL;
19
20
  const describePostgres = databaseUrl ? describe : describe.skip;
@@ -38,6 +39,7 @@ describePostgres("PostgreSQL durable execution", () => {
38
39
  await pool.query(`truncate table
39
40
  governed_export_requests,
40
41
  bulk_data_artifacts, bulk_data_jobs, scheduled_task_runs,
42
+ client_agent_workflow_artifacts,
41
43
  durable_job_recoveries, durable_job_failures, durable_job_artifacts,
42
44
  durable_job_progress, durable_job_leases, durable_job_attempts,
43
45
  durable_job_stages, durable_jobs`);
@@ -170,6 +172,69 @@ describePostgres("PostgreSQL durable execution", () => {
170
172
  expect((await service.get(cancelled.job.id))?.artifacts).toEqual([]);
171
173
  });
172
174
 
175
+ test("artifact once 重放首发事实且拒绝覆盖", async () => {
176
+ const service = jobService(pool);
177
+ const submitted = await service.submit(jobInput("pg-artifact-once"));
178
+ const execution = new DrizzleDurableExecutionRepository(drizzle(pool));
179
+ const claim = await execution.claim(claimInput("artifact-once", base));
180
+ if (!claim) throw new Error("artifact once worker 未领取任务");
181
+ const publication = {
182
+ id: "dap_pg_stable",
183
+ jobId: claim.jobId,
184
+ stageId: claim.stageId,
185
+ kind: "result",
186
+ artifactRef: artifactRef("blob_0123456789abcdef0123456789abcdef"),
187
+ filename: "result.json",
188
+ contentType: "application/json",
189
+ byteSize: 10,
190
+ sha256: "a".repeat(64),
191
+ ownerId: "user_pg",
192
+ metadata: { second: 2, first: 1 },
193
+ };
194
+
195
+ await expect(
196
+ execution.publishArtifactOnce(
197
+ claim,
198
+ { ...publication, jobId: "job_forged" },
199
+ base,
200
+ ),
201
+ ).rejects.toBeInstanceOf(DurableArtifactPublicationConflictError);
202
+ await expect(
203
+ execution.publishArtifactOnce(
204
+ claim,
205
+ { ...publication, stageId: "stage_forged" },
206
+ base,
207
+ ),
208
+ ).rejects.toBeInstanceOf(DurableArtifactPublicationConflictError);
209
+ expect((await service.get(submitted.job.id))?.artifacts).toEqual([]);
210
+
211
+ const first = await execution.publishArtifactOnce(claim, publication, base);
212
+ expect(
213
+ await execution.publishArtifactOnce(
214
+ claim,
215
+ { ...publication, metadata: { first: 1, second: 2 } },
216
+ new Date(base.getTime() + 1),
217
+ ),
218
+ ).toEqual(first);
219
+ await expect(
220
+ execution.publishArtifactOnce(
221
+ claim,
222
+ { ...publication, byteSize: 11 },
223
+ new Date(base.getTime() + 2),
224
+ ),
225
+ ).rejects.toBeInstanceOf(DurableArtifactPublicationConflictError);
226
+ await expect(
227
+ execution.publishArtifactOnce(
228
+ claim,
229
+ { ...publication, kind: "other-result" },
230
+ new Date(base.getTime() + 3),
231
+ ),
232
+ ).rejects.toBeInstanceOf(DurableArtifactPublicationConflictError);
233
+ expect((await service.get(submitted.job.id))?.artifacts).toEqual([
234
+ expect.objectContaining({ id: "dap_pg_stable", byteSize: 10 }),
235
+ ]);
236
+ });
237
+
173
238
  test("timeout、running cancel 与人工恢复保留最终事实", async () => {
174
239
  const service = jobService(pool);
175
240
  const submitted = await service.submit({
@@ -27,6 +27,7 @@ describePostgres("PostgreSQL durable job repository", () => {
27
27
  await pool.query(`truncate table
28
28
  governed_export_requests,
29
29
  bulk_data_artifacts, bulk_data_jobs, scheduled_task_runs,
30
+ client_agent_workflow_artifacts,
30
31
  durable_job_recoveries, durable_job_failures, durable_job_artifacts, durable_job_progress,
31
32
  durable_job_leases, durable_job_attempts, durable_job_stages, durable_jobs`);
32
33
  });
@@ -12,6 +12,29 @@ export type LeaseResult =
12
12
  | { ok: true; cancelRequested: boolean; expiresAt: string }
13
13
  | { ok: false; reason: LeaseRejection };
14
14
 
15
+ export type DurableArtifactPublication = Omit<DurableJobArtifact, "createdAt">;
16
+
17
+ export type DurableArtifactPublicationResult =
18
+ | Exclude<LeaseResult, { ok: true }>
19
+ | {
20
+ ok: true;
21
+ cancelRequested: true;
22
+ expiresAt: string;
23
+ }
24
+ | {
25
+ ok: true;
26
+ cancelRequested: false;
27
+ expiresAt: string;
28
+ artifact: DurableJobArtifact;
29
+ };
30
+
31
+ export class DurableArtifactPublicationConflictError extends Error {
32
+ constructor(message = "同一任务产物发布坐标已绑定不同内容") {
33
+ super(message);
34
+ this.name = "DurableArtifactPublicationConflictError";
35
+ }
36
+ }
37
+
15
38
  export interface ClaimDurableJob {
16
39
  queue: string;
17
40
  workerId: string;
@@ -75,6 +98,11 @@ export interface DurableExecutionRepository {
75
98
  artifact: DurableJobArtifact,
76
99
  now: Date,
77
100
  ): Promise<LeaseResult>;
101
+ publishArtifactOnce(
102
+ identity: DurableLeaseIdentity,
103
+ artifact: DurableArtifactPublication,
104
+ now: Date,
105
+ ): Promise<DurableArtifactPublicationResult>;
78
106
  complete(identity: DurableLeaseIdentity, now: Date): Promise<LeaseResult>;
79
107
  fail(
80
108
  identity: DurableLeaseIdentity,
@@ -216,7 +216,9 @@ describe("durable job execution", () => {
216
216
  expiredAt,
217
217
  ),
218
218
  ).toMatchObject({ ok: true });
219
- expect((await service.get(submitted.job.id))?.artifacts).toHaveLength(1);
219
+ expect((await service.get(submitted.job.id))?.artifacts).toEqual([
220
+ expect.objectContaining({ id: "artifact-replay" }),
221
+ ]);
220
222
 
221
223
  const cancelled = await service.submit(jobInput("artifact-cancelled"));
222
224
  const cancelledClaim = await repository.claim(
@@ -17,6 +17,7 @@ import { leaseIdentity, startInMemoryAttempt } from "./in-memory-claim.js";
17
17
  import {
18
18
  inMemoryLeaseSuccess,
19
19
  publishInMemoryArtifact,
20
+ publishInMemoryArtifactOnce,
20
21
  validateInMemoryLease,
21
22
  } from "./in-memory-lease.js";
22
23
  import {
@@ -121,6 +122,21 @@ export class InMemoryDurableExecutionRepository
121
122
  );
122
123
  }
123
124
 
125
+ async publishArtifactOnce(
126
+ identity: DurableLeaseIdentity,
127
+ artifact: Parameters<DurableExecutionRepository["publishArtifactOnce"]>[1],
128
+ now: Date,
129
+ ) {
130
+ return publishInMemoryArtifactOnce(
131
+ this.leases,
132
+ this.artifacts,
133
+ this.jobs,
134
+ identity,
135
+ artifact,
136
+ now,
137
+ );
138
+ }
139
+
124
140
  async complete(identity: DurableLeaseIdentity, now: Date) {
125
141
  const validated = validateInMemoryLease(this.leases, identity, now);
126
142
  if (!validated.ok) return validated;
@@ -4,7 +4,16 @@ import type {
4
4
  DurableLeaseIdentity,
5
5
  } from "@southwind-ai/jobs";
6
6
  import type { LeaseResult } from "./execution-repository.js";
7
+ import type {
8
+ DurableArtifactPublication,
9
+ DurableArtifactPublicationResult,
10
+ } from "./execution-repository.js";
11
+ import { DurableArtifactPublicationConflictError } from "./execution-repository.js";
7
12
  import { hashLeaseToken } from "./in-memory-claim.js";
13
+ import {
14
+ assertArtifactPublicationIdentity,
15
+ sameArtifactPublication,
16
+ } from "./artifact-publication.js";
8
17
 
9
18
  export function validateInMemoryLease(
10
19
  leases: Map<string, DurableJobLease>,
@@ -34,7 +43,7 @@ export function inMemoryLeaseSuccess(
34
43
  jobs: Map<string, { cancelRequestedAt?: string }>,
35
44
  jobId: string,
36
45
  expiresAt: string,
37
- ): LeaseResult {
46
+ ): Extract<LeaseResult, { ok: true }> {
38
47
  return {
39
48
  ok: true,
40
49
  cancelRequested: Boolean(jobs.get(jobId)?.cancelRequestedAt),
@@ -64,3 +73,38 @@ export function publishInMemoryArtifact(
64
73
  );
65
74
  return result;
66
75
  }
76
+
77
+ export function publishInMemoryArtifactOnce(
78
+ leases: Map<string, DurableJobLease>,
79
+ artifacts: Map<string, DurableJobArtifact>,
80
+ jobs: Map<string, { cancelRequestedAt?: string }>,
81
+ identity: DurableLeaseIdentity,
82
+ artifact: DurableArtifactPublication,
83
+ now: Date,
84
+ ): DurableArtifactPublicationResult {
85
+ const validated = validateInMemoryLease(leases, identity, now);
86
+ if (!validated.ok) return validated;
87
+ assertArtifactPublicationIdentity(identity, artifact);
88
+ const lease = inMemoryLeaseSuccess(
89
+ jobs,
90
+ identity.jobId,
91
+ validated.lease.expiresAt,
92
+ );
93
+ if (lease.cancelRequested) {
94
+ return { ...lease, cancelRequested: true };
95
+ }
96
+ const publicationLease = { ...lease, cancelRequested: false } as const;
97
+ const candidate = { ...artifact, createdAt: now.toISOString() };
98
+ const coordinate = `${artifact.jobId}:${artifact.kind}`;
99
+ const existing =
100
+ artifacts.get(coordinate) ||
101
+ [...artifacts.values()].find(({ id }) => id === artifact.id);
102
+ if (existing) {
103
+ if (!sameArtifactPublication(existing, artifact)) {
104
+ throw new DurableArtifactPublicationConflictError();
105
+ }
106
+ return { ...publicationLease, artifact: structuredClone(existing) };
107
+ }
108
+ artifacts.set(coordinate, structuredClone(candidate));
109
+ return { ...publicationLease, artifact: candidate };
110
+ }
@@ -34,9 +34,11 @@ describePostgres("PostgreSQL legacy durable adapter", () => {
34
34
  });
35
35
  await pool.query(`
36
36
  truncate table
37
+ governed_export_requests,
37
38
  bulk_data_artifacts,
38
39
  bulk_data_jobs,
39
40
  scheduled_task_runs,
41
+ client_agent_workflow_artifacts,
40
42
  durable_job_recoveries,
41
43
  durable_job_failures,
42
44
  durable_job_artifacts,
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ const integrationFiles = new Bun.Glob("**/*.integration.test.ts");
4
+ const migrationFiles = new Bun.Glob("*.sql");
5
+
6
+ describe("PostgreSQL durable job test cleanup", () => {
7
+ test("所有显式 durable_jobs TRUNCATE 同批清理受限子表且不使用 CASCADE", async () => {
8
+ const serverSourceRoot = new URL("../", import.meta.url).pathname;
9
+ const migrationsRoot = new URL(
10
+ "../../../../packages/database/drizzle/",
11
+ import.meta.url,
12
+ ).pathname;
13
+ const durableJobChildTables = new Set<string>();
14
+ const harnesses: string[] = [];
15
+
16
+ for await (const path of migrationFiles.scan(migrationsRoot)) {
17
+ const migration = await Bun.file(`${migrationsRoot}/${path}`).text();
18
+ for (const match of migration.matchAll(
19
+ /ALTER TABLE "([^"]+)" ADD CONSTRAINT [^\n]+ REFERENCES "public"\."durable_jobs"/g,
20
+ )) {
21
+ durableJobChildTables.add(match[1]!);
22
+ }
23
+ }
24
+ expect(durableJobChildTables).toContain("client_agent_workflow_artifacts");
25
+
26
+ for await (const path of integrationFiles.scan(serverSourceRoot)) {
27
+ const source = await Bun.file(`${serverSourceRoot}/${path}`).text();
28
+ if (!source.toLowerCase().includes("truncate table")) continue;
29
+ if (!source.includes("durable_jobs")) continue;
30
+
31
+ harnesses.push(path);
32
+ const cleanup = source.match(
33
+ /`(?=[^`]*truncate table)(?=[^`]*durable_jobs)[^`]+`/is,
34
+ )?.[0];
35
+ expect(cleanup, `${path} 缺少可审计的显式清理 SQL`).toBeDefined();
36
+ for (const childTable of durableJobChildTables) {
37
+ expect(cleanup, `${path} 缺少 ${childTable}`).toContain(childTable);
38
+ }
39
+ expect(cleanup, `${path} 不得用 CASCADE 掩盖清理闭包漂移`).not.toMatch(
40
+ /\bcascade\b/i,
41
+ );
42
+ }
43
+
44
+ expect(
45
+ harnesses.length,
46
+ "至少应发现一个 durable_jobs 清理 harness",
47
+ ).toBeGreaterThan(0);
48
+ for (const coreHarness of [
49
+ "jobs/drizzle-execution.integration.test.ts",
50
+ "jobs/drizzle-repository.integration.test.ts",
51
+ "workflow/drizzle-repository.integration.test.ts",
52
+ ]) {
53
+ expect(harnesses, `缺少核心清理 harness:${coreHarness}`).toContain(
54
+ coreHarness,
55
+ );
56
+ }
57
+ });
58
+ });
@@ -4,6 +4,7 @@ import type {
4
4
  LeaseResult,
5
5
  ManualJobRecovery,
6
6
  } from "./execution-repository.js";
7
+ import { createDurableArtifactPublisher } from "./artifact-publisher.js";
7
8
  import {
8
9
  DurableHandlerError,
9
10
  type DurableHandlerContext,
@@ -158,20 +159,13 @@ export class DurableJobWorker {
158
159
  );
159
160
  assertLease(result, abort);
160
161
  },
161
- publishArtifact: async (artifact) => {
162
- const result = await this.repository.publishArtifact(
163
- identity,
164
- {
165
- id: Bun.randomUUIDv7(),
166
- jobId: identity.jobId,
167
- stageId: identity.stageId,
168
- ...artifact,
169
- createdAt: this.now().toISOString(),
170
- },
171
- this.now(),
172
- );
173
- assertLease(result, abort);
174
- },
162
+ ...createDurableArtifactPublisher({
163
+ repository: this.repository,
164
+ identity,
165
+ stageKey: claim.stage.key,
166
+ now: this.now,
167
+ assertLease: (result) => assertLease(result, abort),
168
+ }),
175
169
  };
176
170
  await Promise.race([
177
171
  (async () => {
@@ -0,0 +1,18 @@
1
+ import {
2
+ ModuleBackgroundAiError,
3
+ type ModuleBackgroundAiPort,
4
+ } from "@southwind-ai/server-sdk";
5
+
6
+ export function unavailableModuleBackgroundAiPort(
7
+ moduleName: string,
8
+ ): ModuleBackgroundAiPort {
9
+ return {
10
+ async enqueueOperation() {
11
+ throw new ModuleBackgroundAiError(
12
+ "AI_OPERATION_UNAVAILABLE",
13
+ `模块 ${moduleName} 的后台 AI Operation Port 未挂载`,
14
+ true,
15
+ );
16
+ },
17
+ };
18
+ }
@@ -14,17 +14,14 @@ import type {
14
14
  // @windy-module system.storage begin
15
15
  import type { ModuleReportStagingPortV1 } from "@southwind-ai/server-sdk";
16
16
  // @windy-module system.storage end
17
- import { ModuleBackgroundAiError } from "@southwind-ai/server-sdk";
18
17
  // @windy-module system.storage begin
19
18
  import { ModuleReportStagingError } from "@southwind-ai/server-sdk";
20
19
  // @windy-module system.storage end
21
20
  import { evaluateAccessGuards } from "../access-guards.js";
22
- import {
23
- captureAuthorizationScope,
24
- hashAuthorizationScope,
25
- } from "../data-access/scope-snapshot.js";
21
+ import * as scopeSnapshot from "../data-access/scope-snapshot.js";
26
22
  import type { RequestActor, RequestGuardContext } from "../guards.js";
27
23
  import type { createServerRuntime } from "../runtime.js";
24
+ import { unavailableModuleBackgroundAiPort } from "./background-ai-unavailable.js";
28
25
  import type { InitializedModuleHosts } from "./initialize.js";
29
26
  import {
30
27
  createModuleStoragePort,
@@ -36,6 +33,9 @@ import { createModuleStorageWritePort } from "./storage-write-port.js";
36
33
  import { unavailableModuleStorageWritePort } from "./storage-write-unavailable.js";
37
34
  import { toModuleActor } from "./request-context.js";
38
35
  import { createModuleSecretPort } from "./module-secret-port.js";
36
+ // @windy-module system.agent begin
37
+ import { moduleWorkflowInputArtifactContext } from "./workflow-input-artifact-port.js";
38
+ // @windy-module system.agent end
39
39
  // @windy-module system.storage begin
40
40
  import type { ReportStagingService } from "../report-staging/service.js";
41
41
  import { createModuleReportStagingPort } from "./report-staging-port.js";
@@ -87,8 +87,8 @@ export function createModuleDurableContextualizer(
87
87
  ) {
88
88
  throw denied("MODULE_ACTOR_UNAVAILABLE");
89
89
  }
90
- const current = captureAuthorizationScope(actor.scopeContext);
91
- if (current.hash !== hashAuthorizationScope(snapshot)) {
90
+ const current = scopeSnapshot.captureAuthorizationScope(actor.scopeContext);
91
+ if (current.hash !== scopeSnapshot.hashAuthorizationScope(snapshot)) {
92
92
  throw denied("MODULE_AUTHORIZATION_SCOPE_CHANGED");
93
93
  }
94
94
  const request = new Request(
@@ -216,6 +216,15 @@ export function createModuleDurableContextualizer(
216
216
  }),
217
217
  storage,
218
218
  storageWriter,
219
+ // @windy-module system.agent begin
220
+ ...moduleWorkflowInputArtifactContext(
221
+ input.moduleName,
222
+ actor.id,
223
+ storage,
224
+ services.storage?.blobStore,
225
+ input.context,
226
+ ),
227
+ // @windy-module system.agent end
219
228
  // @windy-module system.storage begin
220
229
  reportStaging,
221
230
  // @windy-module system.storage end
@@ -224,7 +233,7 @@ export function createModuleDurableContextualizer(
224
233
  input.moduleName,
225
234
  guardContext,
226
235
  input.context.signal,
227
- ) ?? unavailableBackgroundAi(input.moduleName),
236
+ ) ?? unavailableModuleBackgroundAiPort(input.moduleName),
228
237
  };
229
238
  return moduleContext;
230
239
  };
@@ -249,8 +258,8 @@ async function resolveDurableReportAccess(
249
258
  const snapshot = input.claim.job.authorizationScope;
250
259
  if (
251
260
  !snapshot ||
252
- captureAuthorizationScope(actor.scopeContext).hash !==
253
- hashAuthorizationScope(snapshot)
261
+ scopeSnapshot.captureAuthorizationScope(actor.scopeContext).hash !==
262
+ scopeSnapshot.hashAuthorizationScope(snapshot)
254
263
  ) {
255
264
  return false;
256
265
  }
@@ -322,18 +331,6 @@ function unavailableDurableStoragePort(
322
331
  };
323
332
  }
324
333
 
325
- function unavailableBackgroundAi(moduleName: string): ModuleBackgroundAiPort {
326
- return {
327
- async enqueueOperation() {
328
- throw new ModuleBackgroundAiError(
329
- "AI_OPERATION_UNAVAILABLE",
330
- `模块 ${moduleName} 的后台 AI Operation Port 未挂载`,
331
- true,
332
- );
333
- },
334
- };
335
- }
336
-
337
334
  function denied(code: string, details?: Record<string, unknown>) {
338
335
  return new DurableHandlerError(
339
336
  code,
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import type { ModuleHost } from "@southwind-ai/server-sdk";
2
+ import {
3
+ // @windy-module system.agent begin
4
+ defineModuleWorkflowInputMaterializationHandlerV1,
5
+ // @windy-module system.agent end
6
+ type ModuleHost,
7
+ } from "@southwind-ai/server-sdk";
3
8
  import { featureKey } from "@southwind-ai/shared";
4
9
  import { initializeModuleHosts } from "./initialize.js";
5
10
  import {
@@ -130,6 +135,80 @@ describe("Server Module 实现完整性", () => {
130
135
  });
131
136
  }
132
137
  });
138
+
139
+ // @windy-module system.agent begin
140
+ test("固定 materialization task 由同 capability Durable Handler 完整实现", async () => {
141
+ const moduleManifest = manifest("case", {
142
+ features: [
143
+ {
144
+ key: featureKey("case.materialize"),
145
+ label: "案件输入物化",
146
+ module: "case",
147
+ visible: "hidden",
148
+ enabled: true,
149
+ stage: "stable",
150
+ },
151
+ ],
152
+ permissions: [
153
+ {
154
+ key: "case.materialize.execute",
155
+ label: "物化案件输入",
156
+ module: "case",
157
+ moduleLabel: "案件",
158
+ resource: "materialization",
159
+ resourceLabel: "输入物化",
160
+ action: "execute",
161
+ },
162
+ ],
163
+ tasks: [
164
+ {
165
+ key: "case.workflow-input-materialize",
166
+ label: "案件输入物化",
167
+ description: "物化受治理 Workflow 输入。",
168
+ featureKey: "case.materialize",
169
+ permissionKey: "case.materialize.execute",
170
+ auditAction: "case.materialize.execute",
171
+ intervalSeconds: 60,
172
+ maxAttempts: 3,
173
+ retryDelaySeconds: 5,
174
+ },
175
+ ],
176
+ auditActions: ["case.materialize.execute"],
177
+ });
178
+ const installation = {
179
+ manifest: moduleManifest,
180
+ setup(host: ModuleHost) {
181
+ host.registerDurableHandler(
182
+ defineModuleWorkflowInputMaterializationHandlerV1({
183
+ moduleName: "case",
184
+ featureKey: "case.materialize",
185
+ permissionKey: "case.materialize.execute",
186
+ auditAction: "case.materialize.execute",
187
+ }),
188
+ );
189
+ },
190
+ };
191
+ const hosts = await initializeModuleHosts({
192
+ modules: [moduleManifest],
193
+ installations: [installation],
194
+ });
195
+ expect(() =>
196
+ validateHostedModuleImplementations([installation], hosts),
197
+ ).not.toThrow();
198
+ expect(() =>
199
+ validateHostedModuleImplementations([installation], {
200
+ ...hosts,
201
+ durableHandlers: hosts.durableHandlers.map((entry) => ({
202
+ ...entry,
203
+ definition: {
204
+ ...entry.definition,
205
+ auditAction: "case.materialize.drifted",
206
+ },
207
+ })),
208
+ }),
209
+ ).toThrow("定时任务 case.workflow-input-materialize 缺少 Handler");
210
+ });
211
+ // @windy-module system.agent end
133
212
  });
134
213
 
135
214
  function completeRouteModule() {
@@ -4,6 +4,9 @@ import type {
4
4
  InstallableServerModule,
5
5
  ModuleGovernedResourceRegistration,
6
6
  } from "@southwind-ai/server-sdk";
7
+ // @windy-module system.agent begin
8
+ import { moduleWorkflowInputMaterializationHandlerKey } from "@southwind-ai/server-sdk";
9
+ // @windy-module system.agent end
7
10
  import type { InitializedModuleHosts } from "./initialize-contracts.js";
8
11
 
9
12
  /**
@@ -42,7 +45,10 @@ export function validateHostedModuleImplementations(
42
45
  installation.manifest.tasks.map(({ key }) => key),
43
46
  hosts.taskHandlers
44
47
  .filter((entry) => entry.moduleName === moduleName)
45
- .map(({ registration }) => registration.taskKey),
48
+ .map(({ registration }) => registration.taskKey)
49
+ // @windy-module system.agent begin
50
+ .concat(materializationDurableTaskKeys(installation.manifest, hosts)),
51
+ // @windy-module system.agent end
46
52
  );
47
53
  assertComplete(
48
54
  "Agent Tool",
@@ -69,6 +75,27 @@ export function validateHostedModuleImplementations(
69
75
  validateDurableHandlerInstallations(hosts);
70
76
  }
71
77
 
78
+ // @windy-module system.agent begin
79
+ function materializationDurableTaskKeys(
80
+ manifest: ModuleManifest,
81
+ hosts: InitializedModuleHosts,
82
+ ) {
83
+ const key = moduleWorkflowInputMaterializationHandlerKey(manifest.name);
84
+ const task = manifest.tasks.find((candidate) => candidate.key === key);
85
+ if (!task) return [];
86
+ return hosts.durableHandlers
87
+ .filter(
88
+ ({ moduleName, definition }) =>
89
+ moduleName === manifest.name &&
90
+ definition.key === key &&
91
+ definition.featureKey === task.featureKey &&
92
+ definition.permissionKey === task.permissionKey &&
93
+ definition.auditAction === task.auditAction,
94
+ )
95
+ .map(({ definition }) => definition.key);
96
+ }
97
+ // @windy-module system.agent end
98
+
72
99
  export function governedResourceRouteKeys(
73
100
  registration: Pick<
74
101
  ModuleGovernedResourceRegistration,
@@ -40,6 +40,9 @@ import {
40
40
  import { unavailableModuleSecretPort } from "./module-secret-port.js";
41
41
  import { unavailableModuleStoragePort } from "./storage-port.js";
42
42
  import { unavailableModuleStorageWritePort } from "./storage-write-unavailable.js";
43
+ // @windy-module system.agent begin
44
+ import { createModuleWorkflowInputMaterializationPort } from "./workflow-input-materialization.js";
45
+ // @windy-module system.agent end
43
46
 
44
47
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
45
48
 
@@ -115,6 +118,17 @@ export interface ModuleRequestContextInput {
115
118
  export function buildModuleRequestContext(
116
119
  input: ModuleRequestContextInput,
117
120
  ): ModuleRequestContext {
121
+ const jobs = createModuleJobPort({
122
+ moduleName: input.moduleName,
123
+ guardContext: input.guardContext,
124
+ runtime: input.runtime,
125
+ jobs: input.jobs,
126
+ control: input.jobControl,
127
+ reauthorize: input.reauthorizeJobs,
128
+ });
129
+ // @windy-module system.agent begin
130
+ const materializationHandlerKey = `${input.moduleName}.workflow-input-materialize`;
131
+ // @windy-module system.agent end
118
132
  return {
119
133
  requestId: input.guardContext.requestId,
120
134
  signal: input.signal ?? new AbortController().signal,
@@ -136,14 +150,24 @@ export function buildModuleRequestContext(
136
150
  ...(input.sourceInvalidation
137
151
  ? { sourceInvalidation: input.sourceInvalidation }
138
152
  : {}),
139
- jobs: createModuleJobPort({
140
- moduleName: input.moduleName,
141
- guardContext: input.guardContext,
142
- runtime: input.runtime,
143
- jobs: input.jobs,
144
- control: input.jobControl,
145
- reauthorize: input.reauthorizeJobs,
146
- }),
153
+ jobs,
154
+ // @windy-module system.agent begin
155
+ ...(input.jobs &&
156
+ input.manifest.tasks.some(({ key }) => key === materializationHandlerKey)
157
+ ? {
158
+ workflowInputMaterialization:
159
+ createModuleWorkflowInputMaterializationPort({
160
+ moduleName: input.moduleName,
161
+ guardContext: input.guardContext,
162
+ jobs: input.jobs,
163
+ moduleJobs: jobs,
164
+ ...(input.reauthorizeJobs
165
+ ? { reauthorize: input.reauthorizeJobs }
166
+ : {}),
167
+ }),
168
+ }
169
+ : {}),
170
+ // @windy-module system.agent end
147
171
  secrets: input.secrets ?? unavailableModuleSecretPort(),
148
172
  storage: input.storage ?? unavailableModuleStoragePort(input.moduleName),
149
173
  storageWriter: