hoi4-agent-tools 3.0.9 → 3.1.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 (124) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +4 -0
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  7. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  8. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  9. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  10. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  11. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  12. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  13. package/dist/hoi4_agent_tools/core/index.js +47 -4
  14. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  15. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  16. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  17. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  18. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  19. package/dist/hoi4_agent_tools/core/job-worker-host.js +200 -0
  20. package/dist/hoi4_agent_tools/core/job-worker.js +62 -0
  21. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  22. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  23. package/dist/hoi4_agent_tools/core/operation-tasks.js +382 -0
  24. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  25. package/dist/hoi4_agent_tools/core/result.js +1 -0
  26. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  27. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  28. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  29. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  30. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  31. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  32. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  33. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  34. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  35. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  36. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  37. package/dist/hoi4_agent_tools/event/service.js +67 -15
  38. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  39. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  40. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  41. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  42. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  43. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  44. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  45. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  46. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  47. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  48. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  49. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  50. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  51. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  52. package/dist/hoi4_agent_tools/mcp/server/create.js +16 -6
  53. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  54. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  55. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  56. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  57. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  58. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +43 -0
  59. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  60. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  61. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +14 -0
  62. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  63. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  64. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  65. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  66. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  67. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  68. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  69. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  70. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  71. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  72. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  73. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  74. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  75. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  76. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  77. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  78. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  79. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  80. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  81. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  82. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  83. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  84. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  85. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  86. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  87. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  88. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  89. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  90. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  91. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  92. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  93. package/dist/hoi4_agent_tools/version.js +1 -1
  94. package/docs/README.md +2 -0
  95. package/docs/development.md +2 -0
  96. package/docs/events.md +7 -2
  97. package/docs/helper-expansion.md +78 -0
  98. package/docs/http.md +2 -0
  99. package/docs/jobs.md +85 -0
  100. package/docs/technology.md +6 -2
  101. package/package.json +11 -5
  102. package/schemas/configuration.schema.json +1 -1
  103. package/schemas/continuous-focus-palette.schema.json +1 -1
  104. package/schemas/custom-weighted-pool.schema.json +1 -1
  105. package/schemas/event-feature-manifest.schema.json +1 -1
  106. package/schemas/focus-plan.schema.json +1 -1
  107. package/schemas/focus-planning-sidecar.schema.json +1 -1
  108. package/schemas/gui-animation-source.schema.json +1 -1
  109. package/schemas/gui-helper.schema.json +1 -1
  110. package/schemas/gui-scenario.schema.json +1 -1
  111. package/schemas/helper-expansion-request.schema.json +44 -0
  112. package/schemas/helper-expansion-summary.schema.json +70 -0
  113. package/schemas/map-operation.schema.json +1 -1
  114. package/schemas/operation-result.schema.json +1 -1
  115. package/schemas/probability-analysis-result.schema.json +1 -1
  116. package/schemas/probability-compare-input.schema.json +1 -1
  117. package/schemas/probability-evaluate-input.schema.json +1 -1
  118. package/schemas/probability-inspect-input.schema.json +1 -1
  119. package/schemas/probability-render-input.schema.json +1 -1
  120. package/schemas/probability-scenario-set.schema.json +1 -1
  121. package/schemas/probability-sequence-input.schema.json +1 -1
  122. package/schemas/probability-simulate-input.schema.json +1 -1
  123. package/schemas/probability-sweep-input.schema.json +1 -1
  124. package/server.json +2 -2
@@ -0,0 +1,126 @@
1
+ import { z } from 'zod/v4';
2
+ import { ServiceError } from './result.js';
3
+ import { executePlannedTransaction, } from './transaction-execution.js';
4
+ const jsonObject = z.record(z.string(), z.json());
5
+ const defaultCompletion = (execution, reconciled) => Promise.resolve(jsonObject.parse({
6
+ execution: execution.outcome,
7
+ reconciled,
8
+ validationPassed: execution.transaction.validation.passed,
9
+ artifacts: [
10
+ ...execution.artifacts,
11
+ ...(execution.transaction.executionArtifacts ?? []),
12
+ ...execution.transaction.artifacts,
13
+ ],
14
+ changedFiles: execution.outcome === 'applied'
15
+ ? execution.transaction.files.map(({ relativePath }) => relativePath)
16
+ : [],
17
+ }));
18
+ /** One shared write boundary for background execution and interrupted-outcome reconciliation. */
19
+ export class JobTransactions {
20
+ engine;
21
+ jobs;
22
+ constructor(engine, jobs) {
23
+ this.engine = engine;
24
+ this.jobs = jobs;
25
+ }
26
+ async execute(workspaceId, id, ownerToken, transaction, principal, signal, recipe, completion = defaultCompletion) {
27
+ const job = await this.owned(workspaceId, id, ownerToken, principal, signal);
28
+ this.assertTransaction(job, transaction);
29
+ if (job.transaction !== undefined) {
30
+ if (job.transaction.transactionId !== transaction.transactionId ||
31
+ job.transaction.planHash !== transaction.planHash)
32
+ throw new ServiceError('JOB_TRANSACTION_CONFLICT', 'The retry does not match the job transaction binding');
33
+ return this.reconcile(workspaceId, id, ownerToken, principal, signal, true, completion);
34
+ }
35
+ return this.apply(job, ownerToken, transaction, principal, signal, recipe, completion);
36
+ }
37
+ async reconcile(workspaceId, id, ownerToken, principal, signal, resumePlanned = true, completion = defaultCompletion) {
38
+ const job = await this.owned(workspaceId, id, ownerToken, principal, signal);
39
+ if (job.transaction === undefined)
40
+ throw new ServiceError('JOB_TRANSACTION_UNBOUND', 'No persisted transaction binding exists for this job');
41
+ const transaction = await this.engine.transactions.recoverTransaction(workspaceId, job.transaction.transactionId, principal, signal);
42
+ this.assertTransaction(job, transaction);
43
+ if (transaction.planHash !== job.transaction.planHash)
44
+ throw new ServiceError('JOB_TRANSACTION_CONFLICT', 'The recorded transaction plan does not match the job binding');
45
+ if (transaction.state === 'applied')
46
+ return this.complete(job, ownerToken, { transaction, outcome: 'applied', artifacts: [] }, true, principal, job.writeRecipe, completion);
47
+ if (transaction.state === 'planned') {
48
+ if (resumePlanned)
49
+ return this.apply(job, ownerToken, transaction, principal, signal, job.writeRecipe, completion);
50
+ if (job.cancelRequested)
51
+ return this.jobs.store.updateOwned(this.jobs.scope(workspaceId, principal), id, ownerToken, { status: 'cancelled', cancelRequested: true });
52
+ return this.fail(job, ownerToken, 'JOB_REWRITE_NOT_APPLIED', 'Execution stopped before source application; the plan was not replayed', principal);
53
+ }
54
+ if (transaction.state === 'rolled_back' || transaction.state === 'failed')
55
+ return this.fail(job, ownerToken, transaction.rollbackStatus === 'applied'
56
+ ? 'JOB_REWRITE_RESTORED'
57
+ : 'JOB_REWRITE_RECOVERY_REQUIRED', transaction.rollbackStatus === 'applied'
58
+ ? 'The interrupted rewrite was restored; it was not replayed'
59
+ : 'The transaction could not establish a completed or restored outcome', principal);
60
+ throw new ServiceError('JOB_TRANSACTION_UNRESOLVED', 'The transaction has no stable outcome after recovery');
61
+ }
62
+ async owned(workspaceId, id, ownerToken, principal, signal) {
63
+ const job = await this.jobs.get(workspaceId, id, principal, signal);
64
+ if (job.owner?.token !== ownerToken)
65
+ throw new ServiceError('JOB_OWNER_CHANGED', 'The job is owned by another execution attempt');
66
+ if (!job.request.mutation || !['running', 'reconciling'].includes(job.status))
67
+ throw new ServiceError('JOB_STATE_CONFLICT', 'An active rewrite job is required');
68
+ return job;
69
+ }
70
+ assertTransaction(job, transaction) {
71
+ if (transaction.workspaceId !== job.scope.workspaceId ||
72
+ (transaction.principal ?? null) !== job.scope.principal ||
73
+ transaction.rootFingerprint !== job.scope.rootFingerprint)
74
+ throw new ServiceError('JOB_TRANSACTION_SCOPE_MISMATCH', 'The transaction does not belong to the authorized job scope');
75
+ }
76
+ async apply(job, ownerToken, transaction, principal, signal, recipe, completion = defaultCompletion) {
77
+ try {
78
+ const execution = await executePlannedTransaction(this.engine, transaction, principal, signal, {
79
+ beforeApply: async (planned) => {
80
+ const bound = await this.jobs.store.updateOwned(this.jobs.scope(job.scope.workspaceId, principal), job.id, ownerToken, {
81
+ transaction: { transactionId: planned.transactionId, planHash: planned.planHash },
82
+ ...(recipe === undefined ? {} : { writeRecipe: recipe }),
83
+ }, signal);
84
+ if (bound.cancelRequested)
85
+ throw new DOMException('Job cancellation was requested before application', 'AbortError');
86
+ },
87
+ });
88
+ return await this.complete(job, ownerToken, execution, false, principal, recipe, completion);
89
+ }
90
+ catch (error) {
91
+ // Publication of the job result can fail after the source commit. Consult the journal,
92
+ // never replay the write merely because the caller did not receive its result.
93
+ const current = await this.jobs.get(job.scope.workspaceId, job.id, principal);
94
+ if (current.status === 'completed')
95
+ return current;
96
+ if (current.owner?.token !== ownerToken)
97
+ throw new ServiceError('JOB_OWNER_CHANGED', 'The job is owned by another execution attempt');
98
+ if (current.transaction !== undefined) {
99
+ const outcome = await this.engine.transactions.recoverTransaction(job.scope.workspaceId, current.transaction.transactionId, principal);
100
+ this.assertTransaction(current, outcome);
101
+ if (outcome.state === 'applied')
102
+ return this.complete(current, ownerToken, { transaction: outcome, outcome: 'applied', artifacts: [] }, true, principal, current.writeRecipe ?? recipe, completion);
103
+ if (outcome.state === 'rolled_back')
104
+ return this.fail(current, ownerToken, 'JOB_REWRITE_RESTORED', 'The rewrite failed and its original source was restored', principal);
105
+ if (outcome.state === 'failed')
106
+ return this.fail(current, ownerToken, 'JOB_REWRITE_RECOVERY_REQUIRED', 'The transaction could not establish a completed or restored outcome', principal);
107
+ }
108
+ if (error.name === 'AbortError') {
109
+ return this.jobs.store.updateOwned(this.jobs.scope(job.scope.workspaceId, principal), job.id, ownerToken, { status: 'cancelled', cancelRequested: true });
110
+ }
111
+ return this.fail(current, ownerToken, error instanceof ServiceError ? error.code : 'JOB_REWRITE_FAILED', error instanceof ServiceError
112
+ ? error.message
113
+ : 'The rewrite failed before a committed outcome could be recorded', principal);
114
+ }
115
+ }
116
+ async complete(job, ownerToken, execution, reconciled, principal, recipe, completion = defaultCompletion) {
117
+ const result = await completion(execution, reconciled, recipe);
118
+ return this.jobs.store.updateOwned(this.jobs.scope(job.scope.workspaceId, principal), job.id, ownerToken, {
119
+ status: 'completed',
120
+ result,
121
+ });
122
+ }
123
+ fail(job, ownerToken, code, message, principal) {
124
+ return this.jobs.store.updateOwned(this.jobs.scope(job.scope.workspaceId, principal), job.id, ownerToken, { status: 'failed', failure: { code, message } });
125
+ }
126
+ }
@@ -0,0 +1,200 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { JobService } from './job-service.js';
6
+ import { jobOwnerLiveness, readCheckpointIdentity } from './job-store.js';
7
+ import { RequestScheduler } from './request-scheduler.js';
8
+ import { SharedRequestCapacity } from './shared-request-capacity.js';
9
+ import { containedGeneratedPath } from './workspace.js';
10
+ import { ServiceError } from './result.js';
11
+ const maximumCheckpointRecoveries = 2;
12
+ const maximumPreDispatchRecoveries = 2;
13
+ /** Bounded, fixed-entry child execution; client cancellation/disconnection never kills a worker. */
14
+ export class JobWorkerHost {
15
+ engine;
16
+ jobs;
17
+ capacity;
18
+ scheduler;
19
+ owner = {};
20
+ constructor(engine, jobs, capacity, scheduler) {
21
+ this.engine = engine;
22
+ this.jobs = jobs;
23
+ this.capacity = capacity;
24
+ this.scheduler = scheduler;
25
+ }
26
+ static async create(engine, jobs) {
27
+ const state = engine.resolver.serverState();
28
+ if (state === undefined)
29
+ throw new ServiceError('JOB_STORAGE_UNAVAILABLE', 'Workers require persistent server state');
30
+ const root = await containedGeneratedPath(state.root, 'job-workers');
31
+ await mkdir(root, { recursive: true, mode: 0o700 });
32
+ const config = engine.resolver.config();
33
+ return new JobWorkerHost(engine, jobs ?? (await JobService.create(engine.resolver)), new SharedRequestCapacity(root, config.maxSharedTools), new RequestScheduler(config.maxConcurrentTools));
34
+ }
35
+ async run(workspaceId, id, principal) {
36
+ const initial = await this.jobs.get(workspaceId, id, principal);
37
+ if (['completed', 'cancelled', 'failed'].includes(initial.status))
38
+ return initial;
39
+ if (![
40
+ 'hoi4.event_inspect',
41
+ 'hoi4.event_render',
42
+ 'hoi4.event_compare',
43
+ 'hoi4.tech_inspect',
44
+ 'hoi4.tech_render',
45
+ 'hoi4.tech_compare',
46
+ 'hoi4.probability_inspect',
47
+ 'hoi4.probability_evaluate',
48
+ 'hoi4.probability_sweep',
49
+ 'hoi4.probability_simulate',
50
+ 'hoi4.probability_sequence',
51
+ 'hoi4.probability_compare',
52
+ 'hoi4.probability_render',
53
+ 'hoi4.map_inspect',
54
+ 'hoi4.map_render',
55
+ 'hoi4.map_rewrite',
56
+ 'hoi4.gui_inspect',
57
+ 'hoi4.gui_render',
58
+ 'hoi4.gui_rewrite',
59
+ 'hoi4.focus_inspect',
60
+ 'hoi4.focus_render',
61
+ 'hoi4.focus_raster',
62
+ 'hoi4.focus_rewrite',
63
+ ].includes(initial.request.toolName))
64
+ throw new ServiceError('JOB_OPERATION_UNAVAILABLE', 'This domain has not yet been registered for worker execution');
65
+ const signal = new AbortController().signal;
66
+ const recovered = new Set();
67
+ let preDispatchRecoveries = 0;
68
+ for (;;) {
69
+ try {
70
+ await this.scheduler.run(this.owner, 1024, signal, () => this.capacity.run(signal, (lease) => this.launch(workspaceId, id, lease, principal)));
71
+ return await this.jobs.get(workspaceId, id, principal);
72
+ }
73
+ catch (error) {
74
+ const current = await this.jobs.get(workspaceId, id, principal);
75
+ if (error instanceof ServiceError &&
76
+ error.code === 'REQUEST_LEASE_HANDOFF_LOST' &&
77
+ (current.status === 'queued' ||
78
+ (current.owner !== undefined && jobOwnerLiveness(current.owner) === 'dead')) &&
79
+ preDispatchRecoveries < maximumPreDispatchRecoveries) {
80
+ // No job request was sent to the child: another bounded admission attempt
81
+ // cannot replay a read or a rewrite.
82
+ preDispatchRecoveries += 1;
83
+ await delay(preDispatchRecoveries * 100);
84
+ continue;
85
+ }
86
+ const checkpoint = readCheckpointIdentity(current);
87
+ if (checkpoint !== undefined &&
88
+ current.owner !== undefined &&
89
+ jobOwnerLiveness(current.owner) === 'dead' &&
90
+ recovered.size < maximumCheckpointRecoveries &&
91
+ !recovered.has(checkpoint)) {
92
+ // A replacement revalidates source identity and checkpoint bytes in the normal
93
+ // executor. Never replay writes, repeat a stalled frontier, or reclaim a live owner.
94
+ recovered.add(checkpoint);
95
+ continue;
96
+ }
97
+ return await this.jobs.failInterrupted(workspaceId, id, {
98
+ code: error instanceof ServiceError ? error.code : 'JOB_WORKER_FAILED',
99
+ message: error instanceof ServiceError
100
+ ? error.message
101
+ : 'The isolated worker stopped without publishing a result',
102
+ }, principal);
103
+ }
104
+ }
105
+ }
106
+ async launch(workspaceId, id, lease, principal) {
107
+ // Resolve the same configured workspace and grants again immediately before dispatch.
108
+ await this.jobs.get(workspaceId, id, principal);
109
+ const sourceMode = import.meta.url.endsWith('.ts');
110
+ const entry = fileURLToPath(new URL(sourceMode ? './job-worker.ts' : './job-worker.js', import.meta.url));
111
+ const configuration = this.engine.resolver.config();
112
+ const registeredWorkspaceIds = new Set(configuration.workspaces.map((workspace) => workspace.id));
113
+ const resolvedWorkspaces = this.engine.resolver.list();
114
+ const discoveredWorkspaceIds = resolvedWorkspaces
115
+ .map((workspace) => workspace.id)
116
+ .filter((workspaceId) => !registeredWorkspaceIds.has(workspaceId));
117
+ const preserveDiscoveredGrants = (grant) => grant.allowDiscoveredMods
118
+ ? {
119
+ ...grant,
120
+ workspaceIds: [...new Set([...grant.workspaceIds, ...discoveredWorkspaceIds])],
121
+ }
122
+ : grant;
123
+ const child = spawn(process.execPath, [...(sourceMode ? ['--import', import.meta.resolve('tsx')] : []), entry], {
124
+ detached: true,
125
+ cwd: process.cwd(),
126
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
127
+ windowsHide: true,
128
+ });
129
+ const initialized = new Promise((resolve, reject) => {
130
+ child.once('error', reject);
131
+ child.once('exit', () => reject(new ServiceError('JOB_WORKER_STARTUP_EXIT', 'The worker exited before completing its readiness handshake')));
132
+ child.once('message', (value) => {
133
+ void (async () => {
134
+ if (typeof value !== 'object' ||
135
+ value === null ||
136
+ !('type' in value) ||
137
+ value.type !== 'ready' ||
138
+ child.pid === undefined)
139
+ throw new ServiceError('JOB_WORKER_PROTOCOL', 'The worker did not provide its readiness handshake');
140
+ await lease.handoffToProcess(child.pid);
141
+ await new Promise((resolve, reject) => {
142
+ child.once('disconnect', resolve);
143
+ child.send({
144
+ configuration: {
145
+ ...configuration,
146
+ modRoots: [],
147
+ workspaces: resolvedWorkspaces.map(({ registration }) => registration),
148
+ http: {
149
+ ...configuration.http,
150
+ tokens: configuration.http.tokens.map(preserveDiscoveredGrants),
151
+ principals: configuration.http.principals.map(preserveDiscoveredGrants),
152
+ },
153
+ },
154
+ workspaceId,
155
+ jobId: id,
156
+ ...(principal === undefined ? {} : { principal }),
157
+ }, (error) => {
158
+ if (error === null) {
159
+ // IPC is startup-only. Closing it after dispatch prevents launcher
160
+ // lifetime from becoming an implicit cancellation channel.
161
+ if (child.connected)
162
+ child.disconnect();
163
+ resolve();
164
+ }
165
+ else
166
+ reject(error);
167
+ });
168
+ });
169
+ resolve();
170
+ })().catch((error) => {
171
+ if (child.connected)
172
+ child.disconnect();
173
+ reject(error instanceof Error
174
+ ? error
175
+ : new ServiceError('JOB_WORKER_PROTOCOL', 'Worker initialization failed'));
176
+ });
177
+ });
178
+ });
179
+ await initialized;
180
+ for (;;) {
181
+ const record = await this.jobs.get(workspaceId, id, principal);
182
+ if (['completed', 'failed', 'cancelled'].includes(record.status))
183
+ return;
184
+ if (record.owner !== undefined && record.owner.pid !== child.pid) {
185
+ if (jobOwnerLiveness(record.owner) === 'alive')
186
+ return;
187
+ }
188
+ if (child.pid !== undefined) {
189
+ try {
190
+ process.kill(child.pid, 0);
191
+ }
192
+ catch (error) {
193
+ if (error.code === 'ESRCH')
194
+ throw new ServiceError('JOB_WORKER_EXIT', 'The worker stopped without publishing a terminal job outcome');
195
+ }
196
+ }
197
+ await delay(100);
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from 'zod/v4';
2
+ import { serverConfigurationSchema } from './configuration.js';
3
+ import { CoreEngine } from './engine.js';
4
+ import { JobExecutor, JobOperations } from './job-executor.js';
5
+ import { JobService } from './job-service.js';
6
+ import { WorkspaceResolver } from './workspace.js';
7
+ import { registerEventJobs } from '../event/job-operations.js';
8
+ import { registerFocusJobs } from '../focus/job-operations.js';
9
+ import { registerGuiJobs } from '../gui/job-operations.js';
10
+ import { registerMapJobs } from '../map/job-operations.js';
11
+ import { registerProbabilityJobs } from '../probability/job-operations.js';
12
+ import { registerTechnologyJobs } from '../technology/job-operations.js';
13
+ // Fixed internal process entry point. It accepts only a trusted host's structured IPC
14
+ // message, never a script path, module name, callback, command, or transport request.
15
+ const inputSchema = z
16
+ .object({
17
+ configuration: serverConfigurationSchema,
18
+ workspaceId: z.string().min(1),
19
+ jobId: z.string().regex(/^job_[a-f0-9]{64}$/u),
20
+ principal: z.string().min(1).optional(),
21
+ })
22
+ .strict();
23
+ if (process.send === undefined)
24
+ throw new Error('Job workers require their internal IPC host');
25
+ let started = false;
26
+ const startup = setTimeout(() => {
27
+ if (!started)
28
+ process.exit(1);
29
+ }, 30_000);
30
+ startup.unref();
31
+ process.once('disconnect', () => {
32
+ if (!started)
33
+ process.exit(0);
34
+ });
35
+ process.once('message', (value) => {
36
+ started = true;
37
+ clearTimeout(startup);
38
+ void (async () => {
39
+ const input = inputSchema.parse(value);
40
+ const resolver = await WorkspaceResolver.create(input.configuration);
41
+ const engine = new CoreEngine(resolver);
42
+ // Recovery is targeted by the job executor. Do not sweep or interfere with
43
+ // unrelated live rewrite journals when a worker starts.
44
+ const jobs = await JobService.create(resolver);
45
+ const operations = new JobOperations();
46
+ registerEventJobs(operations, engine);
47
+ registerTechnologyJobs(operations, engine);
48
+ registerProbabilityJobs(operations, engine);
49
+ registerMapJobs(operations, engine);
50
+ registerGuiJobs(operations, engine);
51
+ registerFocusJobs(operations, engine);
52
+ await new JobExecutor(engine, jobs, operations).run(input.workspaceId, input.jobId, input.principal);
53
+ })()
54
+ .catch(() => {
55
+ process.exitCode = 1;
56
+ })
57
+ .finally(() => {
58
+ if (process.connected)
59
+ process.disconnect();
60
+ });
61
+ });
62
+ process.send({ type: 'ready' });
@@ -0,0 +1,31 @@
1
+ import { lstat } from 'node:fs/promises';
2
+ import { ServiceError } from './result.js';
3
+ export async function resolveOperationWorkspaceId(engine, context, workspaceId, signal) {
4
+ if (workspaceId === 'current' && context.resolveCurrentWorkspaceId !== undefined) {
5
+ return context.resolveCurrentWorkspaceId(signal);
6
+ }
7
+ return engine.resolver.resolveWorkspaceId(workspaceId, context.principal);
8
+ }
9
+ export async function resolveOperationWorkspaceForSource(engine, context, workspaceId, relativePath, signal) {
10
+ const resolvedWorkspaceId = await resolveOperationWorkspaceId(engine, context, workspaceId, signal);
11
+ if (workspaceId !== 'current' || relativePath === undefined)
12
+ return resolvedWorkspaceId;
13
+ try {
14
+ const resolved = await engine.resolver.resolvePath(resolvedWorkspaceId, relativePath, 'read', ['mod'], context.principal);
15
+ signal?.throwIfAborted();
16
+ if ((await lstat(resolved.path)).isFile())
17
+ return resolvedWorkspaceId;
18
+ }
19
+ catch (error) {
20
+ if (error.name === 'AbortError')
21
+ throw error;
22
+ }
23
+ return ((await engine.resolver.resolveUniqueModSourceWorkspaceId(relativePath, context.principal, signal)) ?? resolvedWorkspaceId);
24
+ }
25
+ export function requireOperationScope(context, scope) {
26
+ if (context.scopes !== undefined && !context.scopes.includes(scope)) {
27
+ throw new ServiceError('AUTH_SCOPE_REQUIRED', `This operation requires the ${scope} scope`, {
28
+ requiredScope: scope,
29
+ });
30
+ }
31
+ }