cloving 1.4.0 → 1.5.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 (167) hide show
  1. package/README.md +100 -15
  2. package/dist/src/broker/actor/authority-health.d.ts +9 -0
  3. package/dist/src/broker/actor/authority-health.js +24 -0
  4. package/dist/src/broker/actor/authority-runtime.d.ts +4 -2
  5. package/dist/src/broker/actor/authority-runtime.js +29 -20
  6. package/dist/src/broker/actor/authority-session.d.ts +6 -19
  7. package/dist/src/broker/actor/authority-session.js +66 -88
  8. package/dist/src/broker/actor/authority-settings.d.ts +2 -1
  9. package/dist/src/broker/actor/authority-settings.js +4 -1
  10. package/dist/src/broker/actor/authority-upgrades.d.ts +5 -1
  11. package/dist/src/broker/actor/authority-upgrades.js +2 -1
  12. package/dist/src/broker/actor/authority-watchdog.d.ts +6 -2
  13. package/dist/src/broker/actor/authority-watchdog.js +31 -9
  14. package/dist/src/broker/actor/authority.js +3 -1
  15. package/dist/src/broker/actor/projection-memory.d.ts +43 -0
  16. package/dist/src/broker/actor/projection-memory.js +207 -0
  17. package/dist/src/broker/actor/runtime-parts.d.ts +5 -0
  18. package/dist/src/broker/actor/runtime-parts.js +8 -3
  19. package/dist/src/broker/authority-restore.js +1 -1
  20. package/dist/src/broker/effect-upgrade.d.ts +1 -0
  21. package/dist/src/broker/effect-upgrade.js +4 -1
  22. package/dist/src/cli/arguments.js +1 -0
  23. package/dist/src/cli/client.d.ts +3 -1
  24. package/dist/src/cli/client.js +4 -4
  25. package/dist/src/cli/command-requests.d.ts +30 -1
  26. package/dist/src/cli/command-requests.js +43 -2
  27. package/dist/src/cli/commands.js +57 -48
  28. package/dist/src/cli/config-notice.js +5 -2
  29. package/dist/src/cli/doctor-retention.d.ts +2 -0
  30. package/dist/src/cli/doctor-retention.js +93 -0
  31. package/dist/src/cli/doctor.js +5 -0
  32. package/dist/src/cli/init-prompt.js +1 -1
  33. package/dist/src/cli/mcp-server.js +4 -4
  34. package/dist/src/cli/mcp-tools.d.ts +14 -14
  35. package/dist/src/cli/mcp-tools.js +212 -50
  36. package/dist/src/cli/queue-arguments.d.ts +15 -0
  37. package/dist/src/cli/queue-arguments.js +31 -0
  38. package/dist/src/cli/queue-render.d.ts +3 -0
  39. package/dist/src/cli/queue-render.js +26 -0
  40. package/dist/src/cli/reload-render.d.ts +2 -0
  41. package/dist/src/cli/reload-render.js +12 -0
  42. package/dist/src/cli/render.js +37 -8
  43. package/dist/src/cli/report-github-reader.js +1 -1
  44. package/dist/src/cli/runtime-arguments.d.ts +17 -3
  45. package/dist/src/cli/runtime-arguments.js +23 -19
  46. package/dist/src/cli/toolchain.d.ts +1 -0
  47. package/dist/src/cli/toolchain.js +1 -0
  48. package/dist/src/cli/usage.d.ts +2 -2
  49. package/dist/src/cli/usage.js +40 -5
  50. package/dist/src/cli/watch-api-document.d.ts +430 -0
  51. package/dist/src/cli/watch-api-document.js +291 -0
  52. package/dist/src/cli/watch-api-run.d.ts +56 -0
  53. package/dist/src/cli/watch-api-run.js +112 -0
  54. package/dist/src/cli/watch-api.d.ts +17 -0
  55. package/dist/src/cli/watch-api.js +230 -0
  56. package/dist/src/cli/watch-header-view.d.ts +1 -0
  57. package/dist/src/cli/watch-header-view.js +9 -1
  58. package/dist/src/cli/watch-model.d.ts +1 -0
  59. package/dist/src/cli/watch-model.js +3 -0
  60. package/dist/src/cli/watch-page.js +3 -1
  61. package/dist/src/cli/watch-reads.d.ts +1 -0
  62. package/dist/src/cli/watch-reads.js +6 -5
  63. package/dist/src/cli/watch-render.d.ts +1 -0
  64. package/dist/src/cli/watch-server.js +18 -4
  65. package/dist/src/cli.js +50 -43
  66. package/dist/src/configuration-display.js +2 -0
  67. package/dist/src/configuration-schema.d.ts +5 -0
  68. package/dist/src/daemon/claude-recovery.js +13 -9
  69. package/dist/src/daemon/claude-worker.d.ts +3 -3
  70. package/dist/src/daemon/claude-worker.js +10 -11
  71. package/dist/src/daemon/daemon-adapters.d.ts +7 -0
  72. package/dist/src/daemon/daemon-adapters.js +7 -3
  73. package/dist/src/daemon/daemon-loops.d.ts +2 -0
  74. package/dist/src/daemon/daemon-loops.js +17 -11
  75. package/dist/src/daemon/daemon.js +71 -60
  76. package/dist/src/daemon/github-attach.js +77 -4
  77. package/dist/src/daemon/github-calls.js +1 -1
  78. package/dist/src/daemon/github-methods.js +6 -2
  79. package/dist/src/daemon/issue-queue.d.ts +3 -0
  80. package/dist/src/daemon/issue-queue.js +66 -73
  81. package/dist/src/daemon/merge-turns.d.ts +10 -0
  82. package/dist/src/daemon/merge-turns.js +37 -0
  83. package/dist/src/daemon/queue-control.d.ts +13 -0
  84. package/dist/src/daemon/queue-control.js +86 -0
  85. package/dist/src/daemon/queue-journal.d.ts +20 -0
  86. package/dist/src/daemon/queue-journal.js +89 -0
  87. package/dist/src/daemon/record-archive.d.ts +19 -0
  88. package/dist/src/daemon/record-archive.js +82 -0
  89. package/dist/src/daemon/record-sweep.d.ts +24 -0
  90. package/dist/src/daemon/record-sweep.js +147 -0
  91. package/dist/src/daemon/retrying-turn.js +4 -4
  92. package/dist/src/daemon/settings-reload.d.ts +26 -0
  93. package/dist/src/daemon/settings-reload.js +82 -0
  94. package/dist/src/daemon/turn-details.d.ts +4 -0
  95. package/dist/src/daemon/turn-details.js +33 -0
  96. package/dist/src/daemon/worker-profile.d.ts +1 -0
  97. package/dist/src/daemon/worker-profile.js +4 -0
  98. package/dist/src/daemon/workflow-generations.d.ts +7 -4
  99. package/dist/src/daemon/workflow-generations.js +22 -14
  100. package/dist/src/daemon/workflow-reload.d.ts +41 -0
  101. package/dist/src/daemon/workflow-reload.js +244 -0
  102. package/dist/src/daemon/workflow-replay.d.ts +2 -1
  103. package/dist/src/daemon/workflow-replay.js +7 -3
  104. package/dist/src/doctor-report.d.ts +2 -2
  105. package/dist/src/doctor-report.js +2 -0
  106. package/dist/src/private-config.schema.json +31 -0
  107. package/dist/src/project-settings.d.ts +15 -1
  108. package/dist/src/project-settings.js +33 -1
  109. package/dist/src/queue-state.d.ts +26 -0
  110. package/dist/src/queue-state.js +34 -0
  111. package/dist/src/record-retention.d.ts +46 -0
  112. package/dist/src/record-retention.js +81 -0
  113. package/dist/src/service/active-tickets.js +5 -0
  114. package/dist/src/service/dispatcher.d.ts +4 -0
  115. package/dist/src/service/dispatcher.js +33 -0
  116. package/dist/src/service/queue-handlers.d.ts +4 -0
  117. package/dist/src/service/queue-handlers.js +32 -0
  118. package/dist/src/service/registry.d.ts +28 -2
  119. package/dist/src/service/registry.js +48 -0
  120. package/dist/src/service/replies.d.ts +16 -9
  121. package/dist/src/service/replies.js +5 -2
  122. package/dist/src/service/run-handlers.js +15 -10
  123. package/dist/src/service/service.d.ts +7 -3
  124. package/dist/src/service/service.js +16 -31
  125. package/dist/src/service/worker-totals.d.ts +4 -0
  126. package/dist/src/service/worker-totals.js +7 -0
  127. package/dist/src/service/workflow-reload.d.ts +35 -0
  128. package/dist/src/service/workflow-reload.js +52 -0
  129. package/dist/src/storage/decision-snapshots.d.ts +1 -2
  130. package/dist/src/storage/decision-snapshots.js +1 -7
  131. package/dist/src/storage/domain-changes.d.ts +1 -1
  132. package/dist/src/storage/domain-changes.js +1 -0
  133. package/dist/src/storage/domain-schema.d.ts +4 -1
  134. package/dist/src/storage/domain-schema.js +6 -2
  135. package/dist/src/storage/effect-schema.d.ts +1 -0
  136. package/dist/src/storage/effect-schema.js +4 -0
  137. package/dist/src/storage/projection-changes.d.ts +12 -0
  138. package/dist/src/storage/projection-changes.js +194 -0
  139. package/dist/src/storage/projection-lookups.d.ts +7 -0
  140. package/dist/src/storage/projection-lookups.js +30 -0
  141. package/dist/src/workflow-declaration.d.ts +1 -0
  142. package/dist/src/workflow-declaration.js +3 -0
  143. package/dist/tools/compile-workflows.js +3 -0
  144. package/dist/tools/project-configuration.d.ts +7 -2
  145. package/dist/tools/project-configuration.js +30 -1
  146. package/dist/tools/self-host-processes.js +37 -13
  147. package/dist/tools/self-host-project.d.ts +2 -0
  148. package/dist/tools/self-host-project.js +21 -3
  149. package/dist/tools/self-host.js +34 -24
  150. package/dist/tools/workflow-candidate-program.d.ts +4 -2
  151. package/dist/tools/workflow-candidate-program.js +30 -11
  152. package/dist/tools/workflow-candidate-source.d.ts +1 -0
  153. package/dist/tools/workflow-candidate-source.js +11 -3
  154. package/package.json +2 -2
  155. package/templates/develop/README.md +81 -4
  156. package/templates/develop/develop/flow-tests.ts +1 -1
  157. package/templates/develop/develop/interruption-tests.ts +157 -0
  158. package/templates/develop/develop/local.ts +1 -0
  159. package/templates/develop/develop/option-tests.ts +138 -9
  160. package/templates/develop/develop/red-green.ts +55 -0
  161. package/templates/develop/develop/report.ts +31 -11
  162. package/templates/develop/develop/round.ts +1 -0
  163. package/templates/develop/develop/scenarios.ts +27 -0
  164. package/templates/develop/develop/turn.ts +26 -6
  165. package/templates/develop/develop.declaration.json +1 -1
  166. package/templates/develop/develop.test.ts +4 -0
  167. package/templates/develop/template.json +29 -2
@@ -0,0 +1,46 @@
1
+ export interface StoredRun {
2
+ readonly status: string;
3
+ readonly endedAt: number | null;
4
+ }
5
+ export interface RecordFolder {
6
+ readonly started: unknown;
7
+ readonly changedAt: number;
8
+ }
9
+ export interface RetentionContext {
10
+ readonly runs: Readonly<Record<string, StoredRun>>;
11
+ readonly now: number;
12
+ readonly days: number;
13
+ }
14
+ export type RetentionVerdict = "due" | "kept" | "skipped" | "unreadable";
15
+ export interface RetentionSettings {
16
+ readonly days: number | null;
17
+ readonly action: "archive" | "delete";
18
+ readonly archiveFolder: string | null;
19
+ }
20
+ export interface RunTurns {
21
+ readonly runId: string;
22
+ readonly operationIds: readonly string[];
23
+ }
24
+ export type TurnDetails = {
25
+ readonly state: "archived";
26
+ readonly folder: string;
27
+ } | {
28
+ readonly state: "deleted";
29
+ };
30
+ export interface TurnFolders {
31
+ readonly turns: number;
32
+ readonly kept: number;
33
+ readonly archived: string | undefined;
34
+ }
35
+ export declare const sharedRecordFolders: readonly string[];
36
+ export declare const defaultRetention: RetentionSettings;
37
+ type Variables = Readonly<Record<string, string | undefined>>;
38
+ export declare function folderRunId(started: unknown): string | undefined;
39
+ export declare function retentionVerdict(folder: RecordFolder, context: RetentionContext): RetentionVerdict;
40
+ export declare function turnDetails(found: TurnFolders, context: {
41
+ readonly run: StoredRun | undefined;
42
+ readonly retention: RetentionSettings;
43
+ readonly now: number;
44
+ }): TurnDetails | undefined;
45
+ export declare function retentionSettings(variables: Variables): RetentionSettings;
46
+ export {};
@@ -0,0 +1,81 @@
1
+ import { operationScope } from "./operation-identity.js";
2
+ export const sharedRecordFolders = [
3
+ "journal/runs",
4
+ "journal/targets",
5
+ ];
6
+ export const defaultRetention = {
7
+ days: 30,
8
+ action: "archive",
9
+ archiveFolder: null,
10
+ };
11
+ const dayMilliseconds = 86_400_000;
12
+ const finished = ["completed", "failed", "cancelled"];
13
+ export function folderRunId(started) {
14
+ if (typeof started !== "object" || started === null)
15
+ return undefined;
16
+ if ("runId" in started && typeof started.runId === "string")
17
+ return started.runId;
18
+ if (!("operationId" in started) || typeof started.operationId !== "string")
19
+ return undefined;
20
+ return operationScope(started.operationId)?.run;
21
+ }
22
+ export function retentionVerdict(folder, context) {
23
+ const runId = folderRunId(folder.started);
24
+ const run = runId !== undefined && Object.hasOwn(context.runs, runId)
25
+ ? context.runs[runId]
26
+ : undefined;
27
+ if (run === undefined)
28
+ return "unreadable";
29
+ if (!finished.includes(run.status))
30
+ return "skipped";
31
+ const limit = context.now - context.days * dayMilliseconds;
32
+ return endedBefore(run, limit) && folder.changedAt < limit ? "due" : "kept";
33
+ }
34
+ function endedBefore(run, limit) {
35
+ return finished.includes(run.status) && (run.endedAt ?? Number.NaN) < limit;
36
+ }
37
+ export function turnDetails(found, context) {
38
+ if (found.archived !== undefined)
39
+ return { state: "archived", folder: found.archived };
40
+ const { run, retention } = context;
41
+ if (found.turns === 0 || found.kept > 0 || run === undefined)
42
+ return undefined;
43
+ if (retention.action !== "delete" || retention.days === null)
44
+ return undefined;
45
+ const limit = context.now - retention.days * dayMilliseconds;
46
+ return endedBefore(run, limit) ? { state: "deleted" } : undefined;
47
+ }
48
+ function refused(variable, expected) {
49
+ throw new Error(`invalid-setting: ${variable} must be ${expected}`);
50
+ }
51
+ function retentionDays(value) {
52
+ if (value === undefined)
53
+ return defaultRetention.days;
54
+ if (value === "null")
55
+ return null;
56
+ const days = Number(value);
57
+ if (/^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(days))
58
+ return days;
59
+ return refused("CLOVING_RETENTION_DAYS", "a whole number of at least 1 or null");
60
+ }
61
+ function retentionAction(value) {
62
+ if (value === undefined)
63
+ return defaultRetention.action;
64
+ if (value === "archive" || value === "delete")
65
+ return value;
66
+ return refused("CLOVING_RETENTION_ACTION", "archive or delete");
67
+ }
68
+ function archiveFolder(value) {
69
+ if (value === undefined || value === "null")
70
+ return null;
71
+ if (value.startsWith("/"))
72
+ return value;
73
+ return refused("CLOVING_RETENTION_ARCHIVE_FOLDER", "an absolute path or null");
74
+ }
75
+ export function retentionSettings(variables) {
76
+ return {
77
+ days: retentionDays(variables["CLOVING_RETENTION_DAYS"]),
78
+ action: retentionAction(variables["CLOVING_RETENTION_ACTION"]),
79
+ archiveFolder: archiveFolder(variables["CLOVING_RETENTION_ARCHIVE_FOLDER"]),
80
+ };
81
+ }
@@ -1,4 +1,5 @@
1
1
  import { record } from "../protocol.js";
2
+ import { queueStateFields } from "../queue-state.js";
2
3
  import { queueRun, queueSpend } from "./queue-spend.js";
3
4
  export async function activeWorkflow(context, workflowId) {
4
5
  const workflows = await context.authority.queries.workflowIds();
@@ -22,6 +23,7 @@ export async function queueOverview(context) {
22
23
  if (workflow?.admission === undefined)
23
24
  return {};
24
25
  const active = await activeWorkflow(context, workflow.workflowId);
26
+ const control = context.queue?.control();
25
27
  return {
26
28
  queue: {
27
29
  workflowId: workflow.workflowId,
@@ -34,6 +36,9 @@ export async function queueOverview(context) {
34
36
  : []),
35
37
  ...(workflow.resolver.skippedTickets?.() ?? []),
36
38
  ],
39
+ ...(control === undefined
40
+ ? {}
41
+ : queueStateFields(control, active.length)),
37
42
  },
38
43
  };
39
44
  }
@@ -0,0 +1,4 @@
1
+ import type { Reply, ServiceRequest } from "../protocol.ts";
2
+ import { type Connection } from "./event-handler.ts";
3
+ import { type ServiceContext } from "./replies.ts";
4
+ export declare function dispatcher(context: ServiceContext): (request: ServiceRequest, connection: Connection) => Promise<Reply | undefined>;
@@ -0,0 +1,33 @@
1
+ import { decision, questions } from "./decision-handler.js";
2
+ import { events } from "./event-handler.js";
3
+ import { queueCommand, queueRead } from "./queue-handlers.js";
4
+ import { failed, scheduleRequest } from "./replies.js";
5
+ import { runCommand, status } from "./run-handlers.js";
6
+ import { submissionHandler } from "./submission-handler.js";
7
+ import { reloadUnavailable } from "./workflow-reload.js";
8
+ export function dispatcher(context) {
9
+ const run = submissionHandler(context);
10
+ const operations = new Map([
11
+ ["run", (request) => run(request.parameters)],
12
+ ["schedule", (request) => scheduleRequest(context, request.parameters)],
13
+ ["queue", () => queueRead(context)],
14
+ ["queue-command", (request) => queueCommand(context, request.parameters)],
15
+ ["status", (request) => status(context, request.parameters)],
16
+ ["questions", (request) => questions(context, request.parameters)],
17
+ ["decision", (request) => decision(context, request.parameters)],
18
+ ["run-command", (request) => runCommand(context, request.parameters)],
19
+ ["reload", () => context.reload?.() ?? reloadUnavailable()],
20
+ [
21
+ "events",
22
+ (request, connection) => events(context, { parameters: request.parameters, connection }),
23
+ ],
24
+ ]);
25
+ return (request, connection) => {
26
+ const operation = operations.get(request.operation);
27
+ if (operation === undefined)
28
+ return Promise.resolve(failed("unknown-operation", {
29
+ detail: `The service has no operation ${request.operation}.`,
30
+ }));
31
+ return operation(request, connection);
32
+ };
33
+ }
@@ -0,0 +1,4 @@
1
+ import type { Fields, Reply } from "../protocol.ts";
2
+ import { type ServiceContext } from "./replies.ts";
3
+ export declare function queueRead(context: ServiceContext): Promise<Reply>;
4
+ export declare function queueCommand(context: ServiceContext, parameters: Fields): Promise<Reply>;
@@ -0,0 +1,32 @@
1
+ import { queueOverview } from "./active-tickets.js";
2
+ import { failed, invalidRequest, isIdentifier, result, } from "./replies.js";
3
+ const noQueue = failed("queue-unavailable", {
4
+ detail: "The service has no queue workflow.",
5
+ });
6
+ export async function queueRead(context) {
7
+ const overview = await queueOverview(context);
8
+ if (overview["queue"] === undefined)
9
+ return noQueue;
10
+ return result("queue", overview);
11
+ }
12
+ export async function queueCommand(context, parameters) {
13
+ const { command, commandId } = parameters;
14
+ if ((command !== "resume" && command !== "drain") || !isIdentifier(commandId))
15
+ return invalidRequest("The queue command request is not valid.");
16
+ if (context.queue === undefined)
17
+ return noQueue;
18
+ const change = await context.queue.change({ command, commandId });
19
+ if (change.kind === "unavailable")
20
+ return failed("queue-unavailable", { commandId, detail: change.detail });
21
+ if (change.kind === "conflicting-command")
22
+ return failed("conflicting-command", {
23
+ commandId,
24
+ detail: "The command ID names another queue command.",
25
+ });
26
+ return result("queue-command", {
27
+ command,
28
+ commandId,
29
+ replayed: change.kind === "replayed",
30
+ ...(await queueOverview(context)),
31
+ });
32
+ }
@@ -2,6 +2,7 @@ import type { CommandWaits, WorkflowStage, WorkflowStages } from "../workflow-de
2
2
  import type { SubmissionContract } from "../submission.ts";
3
3
  import type { ExecutionManifest } from "../workflow-admission.ts";
4
4
  import type { AvailableManifest } from "../broker/actor/delivery-intents.ts";
5
+ import { type WorkflowRefusal } from "../workflow-refusals.ts";
5
6
  export interface WorkflowBundleStore {
6
7
  readonly directory: string;
7
8
  readonly repository: string;
@@ -52,8 +53,33 @@ export interface Registry {
52
53
  readonly bundle: (pinned: ExecutionManifest) => Promise<string | undefined>;
53
54
  readonly available: AvailableManifest;
54
55
  }
55
- export declare function workflowRegistry(workflows: readonly WorkflowRegistration[], options?: {
56
+ export interface RegistryOptions {
56
57
  readonly retainedWorkflows?: readonly WorkflowRegistration[];
57
58
  readonly workflowBundles?: WorkflowBundleStore;
58
59
  readonly retainedWorkflow?: AvailableManifest;
59
- }): Registry;
60
+ }
61
+ export interface ReplaceableRegistry extends Registry {
62
+ readonly replace: (workflows: readonly WorkflowRegistration[]) => void;
63
+ }
64
+ export declare function replaceableRegistry(workflows: readonly WorkflowRegistration[], options?: RegistryOptions): ReplaceableRegistry;
65
+ export declare function workflowRegistry(workflows: readonly WorkflowRegistration[], options?: RegistryOptions): Registry;
66
+ interface WorkflowInventory {
67
+ readonly bundleDigest?: string;
68
+ readonly workflowRefusals: readonly WorkflowRefusal[];
69
+ }
70
+ export interface WorkflowCatalog {
71
+ readonly registry: Registry;
72
+ readonly inventory: () => WorkflowInventory;
73
+ readonly replace: (next: {
74
+ readonly bundleDigest: string;
75
+ readonly workflows: readonly WorkflowRegistration[];
76
+ readonly workflowRefusals: readonly WorkflowRefusal[];
77
+ }) => void;
78
+ readonly replaceRefusals: (refusals: readonly WorkflowRefusal[]) => void;
79
+ }
80
+ export declare function workflowCatalog(options: RegistryOptions & {
81
+ readonly workflows: readonly WorkflowRegistration[];
82
+ readonly bundleDigest?: string;
83
+ readonly workflowRefusals?: readonly WorkflowRefusal[];
84
+ }): WorkflowCatalog;
85
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { workflowPin } from "../broker/workflow-pin.js";
2
2
  import { declaredManifest } from "../execution-pin.js";
3
3
  import { readRetainedWorkflowBundle } from "../../tools/workflow-bundles.js";
4
+ import { workflowRefusals, } from "../workflow-refusals.js";
4
5
  function registered(workflow) {
5
6
  const manifest = declaredManifest(workflow.manifest, workflow);
6
7
  return {
@@ -19,6 +20,30 @@ function availableVersions(workflow) {
19
20
  delete manifest.bundleDigest;
20
21
  return [workflow, { ...workflow, manifest, pin: workflowPin(manifest) }];
21
22
  }
23
+ export function replaceableRegistry(workflows, options = {}) {
24
+ let earlier = [...workflows, ...(options.retainedWorkflows ?? [])];
25
+ let current = workflowRegistry(workflows, options);
26
+ return {
27
+ get workflowIds() {
28
+ return current.workflowIds;
29
+ },
30
+ get workflowStages() {
31
+ return current.workflowStages;
32
+ },
33
+ byId: (workflowId) => current.byId(workflowId),
34
+ byPin: (pin) => current.byPin(pin),
35
+ bundle: (pinned) => current.bundle(pinned),
36
+ available: (pinned) => current.available(pinned),
37
+ replace: (next) => {
38
+ const replacement = workflowRegistry(next, {
39
+ ...options,
40
+ retainedWorkflows: earlier,
41
+ });
42
+ earlier = [...next, ...earlier];
43
+ current = replacement;
44
+ },
45
+ };
46
+ }
22
47
  export function workflowRegistry(workflows, options = {}) {
23
48
  const current = workflows.map(registered);
24
49
  const available = [
@@ -54,3 +79,26 @@ export function workflowRegistry(workflows, options = {}) {
54
79
  },
55
80
  };
56
81
  }
82
+ export function workflowCatalog(options) {
83
+ const registry = replaceableRegistry(options.workflows, options);
84
+ let inventory = {
85
+ ...(options.bundleDigest === undefined
86
+ ? {}
87
+ : { bundleDigest: options.bundleDigest }),
88
+ workflowRefusals: workflowRefusals(options.workflowRefusals),
89
+ };
90
+ return {
91
+ registry,
92
+ inventory: () => inventory,
93
+ replace: (next) => {
94
+ registry.replace(next.workflows);
95
+ inventory = {
96
+ bundleDigest: next.bundleDigest,
97
+ workflowRefusals: next.workflowRefusals,
98
+ };
99
+ },
100
+ replaceRefusals: (refusals) => {
101
+ inventory = { ...inventory, workflowRefusals: refusals };
102
+ },
103
+ };
104
+ }
@@ -1,19 +1,26 @@
1
1
  import type { SchedulePort } from "../schedule-progress.ts";
2
+ import type { QueuePort } from "../queue-state.ts";
2
3
  import type { Authority, CommandRecord, StoreReceipt } from "../broker/actor/authority.ts";
3
4
  import type { Fields, Reply } from "../protocol.ts";
4
5
  import type { ChangeSignal } from "./changes.ts";
5
6
  import type { Gateway } from "./gateway.ts";
6
- import type { Registry } from "./registry.ts";
7
- import { type WorkflowRefusal } from "../workflow-refusals.ts";
7
+ import type { Registry, WorkflowCatalog } from "./registry.ts";
8
8
  import type { CommandEffects } from "../command-settings.ts";
9
- export interface ServiceContext {
9
+ import type { RunTurns, TurnDetails } from "../record-retention.ts";
10
+ export interface ServiceReads {
11
+ readonly workerHarness?: (operationId: string) => Promise<string | undefined>;
12
+ readonly turnDetails?: (turns: RunTurns) => Promise<TurnDetails | undefined>;
13
+ }
14
+ export interface ServiceContext extends ServiceReads {
15
+ readonly version?: string;
10
16
  readonly schedule?: SchedulePort;
17
+ readonly queue?: QueuePort;
11
18
  readonly queueDailySpendLimitUsd?: number;
12
- readonly workerHarness?: (operationId: string) => Promise<string | undefined>;
13
19
  readonly commandEffects?: CommandEffects;
14
20
  readonly authority: Authority;
15
21
  readonly registry: Registry;
16
- readonly workflowRefusals?: readonly WorkflowRefusal[];
22
+ readonly workflowCatalog?: WorkflowCatalog;
23
+ readonly reload?: () => Promise<Reply>;
17
24
  readonly gateway: Gateway;
18
25
  readonly clock: () => string;
19
26
  readonly livenessMilliseconds: number;
@@ -24,13 +31,13 @@ export interface ServiceContext {
24
31
  readonly worldReports?: () => readonly unknown[];
25
32
  readonly changed?: ChangeSignal["next"];
26
33
  }
27
- export declare function queryMetadata(options: {
34
+ export declare function queryMetadata(options: ServiceReads & {
35
+ readonly version?: string;
28
36
  readonly schedule?: SchedulePort;
37
+ readonly queue?: QueuePort;
29
38
  readonly queueDailySpendLimitUsd?: number;
30
- readonly workerHarness?: ServiceContext["workerHarness"];
31
39
  readonly commandEffects?: CommandEffects;
32
- readonly workflowRefusals?: readonly WorkflowRefusal[];
33
- }): Pick<ServiceContext, "schedule" | "workerHarness" | "commandEffects" | "workflowRefusals" | "queueDailySpendLimitUsd">;
40
+ }): Pick<ServiceContext, "schedule" | "queue" | "workerHarness" | "turnDetails" | "commandEffects" | "queueDailySpendLimitUsd" | "version">;
34
41
  export declare function result(kind: string, fields: Fields): Reply;
35
42
  export declare function failed(code: string, extra: Fields): Reply;
36
43
  export declare function invalidRequest(detail: string): Reply;
@@ -1,18 +1,21 @@
1
1
  import { isRecordKey } from "../authority/commands.js";
2
- import { workflowRefusals, } from "../workflow-refusals.js";
3
2
  export function queryMetadata(options) {
4
3
  return {
4
+ ...(options.version === undefined ? {} : { version: options.version }),
5
5
  ...(options.schedule === undefined ? {} : { schedule: options.schedule }),
6
+ ...(options.queue === undefined ? {} : { queue: options.queue }),
6
7
  ...(options.queueDailySpendLimitUsd === undefined
7
8
  ? {}
8
9
  : { queueDailySpendLimitUsd: options.queueDailySpendLimitUsd }),
9
10
  ...(options.workerHarness === undefined
10
11
  ? {}
11
12
  : { workerHarness: options.workerHarness }),
13
+ ...(options.turnDetails === undefined
14
+ ? {}
15
+ : { turnDetails: options.turnDetails }),
12
16
  ...(options.commandEffects === undefined
13
17
  ? {}
14
18
  : { commandEffects: options.commandEffects }),
15
- workflowRefusals: workflowRefusals(options.workflowRefusals),
16
19
  };
17
20
  }
18
21
  const codes = {
@@ -1,14 +1,16 @@
1
1
  import { commandWaitDisplay } from "./command-waits.js";
2
2
  import { scheduleMetadata, scheduledMetadata, metadataOf, outcomeOf, ticketMetadataPage, workflowMetadata, } from "./run-metadata.js";
3
3
  import { linksOf } from "../authority/handoff-chain.js";
4
+ import { terminal } from "../authority/transitions.js";
4
5
  import { runView, unresolvedEffects, } from "../authority/status.js";
5
6
  import { record } from "../protocol.js";
6
7
  import { committed, failed, invalidRequest, isIdentifier, publicReceipt, rejection, result, wholeNumber, } from "./replies.js";
7
- import { usageTotals } from "./worker-totals.js";
8
+ import { turnDetailsOf, usageTotals } from "./worker-totals.js";
8
9
  import { commandFields } from "./command-effects.js";
9
10
  import { decidedOf, feedbackOf } from "./decision-handler.js";
10
11
  import { queueOverview } from "./active-tickets.js";
11
12
  import { queueRunStatuses } from "./queue-runs.js";
13
+ import { workflowFields } from "./workflow-reload.js";
12
14
  async function snapshot(context) {
13
15
  const { authority } = context;
14
16
  const state = await authority.state();
@@ -150,7 +152,9 @@ async function submittedInput(context, digest) {
150
152
  return record(request?.["submission"]) ?? null;
151
153
  }
152
154
  async function listed(context, page) {
153
- const identifiers = Object.keys(page.snapshot.state.runs)
155
+ const identifiers = Object.entries(page.snapshot.state.runs)
156
+ .filter(([, run]) => !page.active || !terminal.includes(run.status))
157
+ .map(([runId]) => runId)
154
158
  .sort()
155
159
  .filter((runId) => runId > page.after);
156
160
  const shown = identifiers.slice(0, page.limit);
@@ -159,15 +163,12 @@ async function listed(context, page) {
159
163
  views.push(await viewOf(context, { snapshot: page.snapshot, runId }));
160
164
  const runs = views.filter((view) => view !== undefined).map(summary);
161
165
  return result("run-list", {
166
+ ...(context.version === undefined
167
+ ? {}
168
+ : { serviceVersion: context.version }),
162
169
  ...(await queueOverview(context)),
163
170
  ...(await scheduleMetadata(context)),
164
- workflows: context.registry.workflowIds,
165
- workflowRefusals: context.workflowRefusals ?? [],
166
- ...(Object.keys(context.registry.workflowStages).length === 0
167
- ? {}
168
- : {
169
- workflowStages: context.registry.workflowStages,
170
- }),
171
+ ...workflowFields(context),
171
172
  runs,
172
173
  next: identifiers.length > shown.length ? (shown.at(-1) ?? null) : null,
173
174
  worldReports: context.worldReports?.() ?? [],
@@ -176,9 +177,12 @@ async function listed(context, page) {
176
177
  function listParameters(parameters) {
177
178
  const after = parameters["after"] ?? "";
178
179
  const limit = parameters["limit"] ?? 50;
180
+ const active = parameters["active"] ?? false;
179
181
  if (typeof after !== "string" || !wholeNumber(limit, 1) || limit > 500)
180
182
  return undefined;
181
- return { after, limit };
183
+ if (typeof active !== "boolean")
184
+ return undefined;
185
+ return { after, limit, active };
182
186
  }
183
187
  export async function status(context, parameters) {
184
188
  if (parameters["metadata"] === "workflows")
@@ -202,6 +206,7 @@ export async function status(context, parameters) {
202
206
  return result("run-status", {
203
207
  run: {
204
208
  ...full({ ...view, ...totals }),
209
+ ...(await turnDetailsOf(context, { runId: view.runId, totals })),
205
210
  ...(await commandFields(context, view.commandOperations)),
206
211
  outcome: await outcomeOf(context, view),
207
212
  decisions: await decidedOf(context, {
@@ -1,7 +1,8 @@
1
1
  import { type Authority, type AuthorityTiming, type Delivery, type EffectSettings, type Refusal } from "../broker/actor/authority.ts";
2
2
  import { type WorkflowRefusal } from "../workflow-refusals.ts";
3
3
  import { type Registry, type WorkflowRegistration, type WorkflowBundleStore } from "./registry.ts";
4
- import { type ServiceContext } from "./replies.ts";
4
+ import { type WorkflowReloadPort } from "./workflow-reload.ts";
5
+ import { type ServiceContext, type ServiceReads } from "./replies.ts";
5
6
  import { type WorkerPort } from "./worker-port.ts";
6
7
  import type { RunWaitTarget } from "../run-wait.ts";
7
8
  import type { CommandEffects } from "../command-settings.ts";
@@ -49,10 +50,11 @@ export interface HostPorts {
49
50
  readonly openGate: WorkerPort["openGate"];
50
51
  readonly registerWait: WorkerPort["registerWait"];
51
52
  }
52
- export interface ServiceOptions {
53
+ export interface ServiceOptions extends ServiceReads {
54
+ readonly version?: string;
53
55
  readonly schedule?: NonNullable<ServiceContext["schedule"]>;
56
+ readonly queue?: NonNullable<ServiceContext["queue"]>;
54
57
  readonly queueDailySpendLimitUsd?: number;
55
- readonly workerHarness?: ServiceContext["workerHarness"];
56
58
  readonly commandEffects?: CommandEffects;
57
59
  readonly stateDirectory: string;
58
60
  readonly runtimeDirectory: string;
@@ -61,6 +63,8 @@ export interface ServiceOptions {
61
63
  readonly workflowBundles?: WorkflowBundleStore;
62
64
  readonly retainedWorkflow?: Registry["available"];
63
65
  readonly workflowRefusals?: readonly WorkflowRefusal[];
66
+ readonly bundleDigest?: string;
67
+ readonly reload?: WorkflowReloadPort;
64
68
  readonly delivery: Delivery;
65
69
  readonly host?: (ports: HostPorts) => Promise<ServiceHost>;
66
70
  readonly timing?: ServiceTiming;
@@ -4,14 +4,12 @@ import { authorityTiming, openAuthority, } from "../broker/actor/authority.js";
4
4
  import { socketAddress } from "../broker/socket-address.js";
5
5
  import {} from "../workflow-refusals.js";
6
6
  import { changeSignal } from "./changes.js";
7
- import { decision, questions } from "./decision-handler.js";
8
- import { events } from "./event-handler.js";
9
7
  import { expiryScan } from "./expiry.js";
10
8
  import { commandGateway, workerCommandBound } from "./gateway.js";
11
- import { workflowRegistry, } from "./registry.js";
12
- import { errorMessage, failed, queryMetadata, scheduleRequest, } from "./replies.js";
13
- import { runCommand, status } from "./run-handlers.js";
14
- import { submissionHandler } from "./submission-handler.js";
9
+ import { workflowCatalog, } from "./registry.js";
10
+ import { reloadOperation } from "./workflow-reload.js";
11
+ import { errorMessage, queryMetadata, } from "./replies.js";
12
+ import { dispatcher } from "./dispatcher.js";
15
13
  import { connectionLimits, listen, reachable } from "./transport.js";
16
14
  import { workerPort } from "./worker-port.js";
17
15
  import { runWaitTarget } from "./run-waits.js";
@@ -56,31 +54,17 @@ async function endpoint(directory) {
56
54
  function wallClock() {
57
55
  return new Date().toISOString();
58
56
  }
59
- function dispatcher(context) {
60
- const run = submissionHandler(context);
61
- const operations = new Map([
62
- ["run", (request) => run(request.parameters)],
63
- ["schedule", (request) => scheduleRequest(context, request.parameters)],
64
- ["status", (request) => status(context, request.parameters)],
65
- ["questions", (request) => questions(context, request.parameters)],
66
- ["decision", (request) => decision(context, request.parameters)],
67
- ["run-command", (request) => runCommand(context, request.parameters)],
68
- [
69
- "events",
70
- (request, connection) => events(context, { parameters: request.parameters, connection }),
71
- ],
72
- ]);
73
- return (request, connection) => {
74
- const operation = operations.get(request.operation);
75
- if (operation === undefined)
76
- return Promise.resolve(failed("unknown-operation", {
77
- detail: `The service has no operation ${request.operation}.`,
78
- }));
79
- return operation(request, connection);
57
+ function catalogContext(options, catalog) {
58
+ return {
59
+ registry: catalog.registry,
60
+ workflowCatalog: catalog,
61
+ ...(options.reload === undefined
62
+ ? {}
63
+ : { reload: reloadOperation({ port: options.reload, catalog }) }),
80
64
  };
81
65
  }
82
66
  async function started(authority, setup) {
83
- const { options, path, timing, registry, gateway, host } = setup;
67
+ const { options, path, timing, catalog, gateway, host } = setup;
84
68
  const clock = options.clock ?? wallClock;
85
69
  const { textBytes, documentBytes } = options.limits ?? serviceLimits;
86
70
  await rm(path, { force: true });
@@ -90,7 +74,7 @@ async function started(authority, setup) {
90
74
  limits: { idleMilliseconds: timing.idleMilliseconds },
91
75
  handle: dispatcher({
92
76
  authority,
93
- registry,
77
+ ...catalogContext(options, catalog),
94
78
  ...queryMetadata(options),
95
79
  gateway,
96
80
  clock,
@@ -138,7 +122,8 @@ export async function startService(options) {
138
122
  if (typeof path !== "string")
139
123
  return path;
140
124
  const timing = options.timing ?? serviceTiming;
141
- const registry = workflowRegistry(options.workflows, options);
125
+ const catalog = workflowCatalog(options);
126
+ const { registry } = catalog;
142
127
  const opening = await openAuthority(options.stateDirectory, {
143
128
  delivery: options.delivery,
144
129
  timing: timing.authority,
@@ -163,7 +148,7 @@ export async function startService(options) {
163
148
  options,
164
149
  path,
165
150
  timing,
166
- registry,
151
+ catalog,
167
152
  gateway,
168
153
  worker,
169
154
  host: host.host,
@@ -3,3 +3,7 @@ import { type WorkerTotals } from "../worker-usage.ts";
3
3
  import type { ServiceContext } from "./replies.ts";
4
4
  export declare function reportFields(text: string | undefined): Fields;
5
5
  export declare function usageTotals(context: ServiceContext, runId: string): Promise<WorkerTotals>;
6
+ export declare function turnDetailsOf(context: ServiceContext, run: {
7
+ readonly runId: string;
8
+ readonly totals: WorkerTotals;
9
+ }): Promise<Fields>;
@@ -35,3 +35,10 @@ export async function usageTotals(context, runId) {
35
35
  }
36
36
  return workerTotals(turns);
37
37
  }
38
+ export async function turnDetailsOf(context, run) {
39
+ const details = await context.turnDetails?.({
40
+ runId: run.runId,
41
+ operationIds: run.totals.workerTurns.map((turn) => turn.operationId),
42
+ });
43
+ return details === undefined ? {} : { turnDetails: details };
44
+ }
@@ -0,0 +1,35 @@
1
+ import type { Fields, Reply } from "../protocol.ts";
2
+ import type { WorkflowRefusal } from "../workflow-refusals.ts";
3
+ import type { Registry, WorkflowCatalog, WorkflowRegistration } from "./registry.ts";
4
+ export interface SettingChanges {
5
+ readonly reloadedFields: readonly string[];
6
+ readonly restartFields: readonly string[];
7
+ }
8
+ export type WorkflowBundleReload = {
9
+ readonly kind: "reloaded";
10
+ readonly bundleDigest: string;
11
+ readonly workflows: readonly WorkflowRegistration[];
12
+ readonly workflowRefusals: readonly WorkflowRefusal[];
13
+ } | {
14
+ readonly kind: "unchanged";
15
+ readonly workflowRefusals: readonly WorkflowRefusal[];
16
+ } | {
17
+ readonly kind: "refused";
18
+ readonly code: string;
19
+ readonly detail: string;
20
+ };
21
+ export type WorkflowReload = (Exclude<WorkflowBundleReload, {
22
+ readonly kind: "refused";
23
+ }> & SettingChanges) | Extract<WorkflowBundleReload, {
24
+ readonly kind: "refused";
25
+ }>;
26
+ export type WorkflowReloadPort = () => Promise<WorkflowReload>;
27
+ export declare function workflowFields(context: {
28
+ readonly registry: Registry;
29
+ readonly workflowCatalog?: WorkflowCatalog;
30
+ }): Fields;
31
+ export declare function reloadOperation(parts: {
32
+ readonly port: WorkflowReloadPort;
33
+ readonly catalog: WorkflowCatalog;
34
+ }): () => Promise<Reply>;
35
+ export declare function reloadUnavailable(): Promise<Reply>;