@atolis-hq/wake 0.3.53 → 0.3.55

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.
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "gfcfd038";
111
+ export const wakeVersion = "g4d17da0";
@@ -1,4 +1,4 @@
1
- import { RunStatus, WorkspaceMode } from '../../execution/index.js';
1
+ import { ActivationClaimConflictError, RunStatus, WorkspaceMode } from '../../execution/index.js';
2
2
  import { correlationId, EventActorKind, } from '../../kernel/index.js';
3
3
  import { WorkflowStatus } from '../../orchestration/index.js';
4
4
  import { WorkStatus } from '../../work/index.js';
@@ -141,15 +141,23 @@ export function createAdvanceOnce(orchestration, execution, resources, clock, de
141
141
  const correlated = await resources.correlationsForWork(selected.workflow.workItemId);
142
142
  const resourceViews = (await Promise.all(correlated.map((entry) => resources.get(entry.resourceId)))).filter((resource) => resource !== null);
143
143
  const ineligible = await runnerIneligibility();
144
- const run = await execution.attempt(selected.activation, {
145
- workItemId: selected.workflow.workItemId,
146
- workflowInstanceId: selected.workflow.workflowInstanceId,
147
- orchestrationGroupId: selected.workflow.orchestrationGroupId,
148
- resources: resourceViews,
149
- sessionPolicy: selected.workflow.parentWorkflowInstanceId === undefined ? 'resume-stage' : 'fresh',
150
- awaitImmediateCompletion: true,
151
- ...(ineligible.size === 0 ? {} : { ineligibleRunners: ineligible }),
152
- });
144
+ let run;
145
+ try {
146
+ run = await execution.attempt(selected.activation, {
147
+ workItemId: selected.workflow.workItemId,
148
+ workflowInstanceId: selected.workflow.workflowInstanceId,
149
+ orchestrationGroupId: selected.workflow.orchestrationGroupId,
150
+ resources: resourceViews,
151
+ sessionPolicy: selected.workflow.parentWorkflowInstanceId === undefined ? 'resume-stage' : 'fresh',
152
+ awaitImmediateCompletion: true,
153
+ ...(ineligible.size === 0 ? {} : { ineligibleRunners: ineligible }),
154
+ });
155
+ }
156
+ catch (error) {
157
+ if (error instanceof ActivationClaimConflictError)
158
+ return { kind: 'no-work' };
159
+ throw error;
160
+ }
153
161
  if (run.status === RunStatus.Succeeded && run.outcome !== undefined) {
154
162
  if (await isDispatchPaused())
155
163
  return { kind: 'paused' };
@@ -2,13 +2,19 @@ import { EventActorKind, EventSourceKind, } from '../../kernel/index.js';
2
2
  import { createActivationExecutionEventDraft } from '../contracts/event-factory.js';
3
3
  import { decodeActivationExecutionEvent, ExecutionEventType, } from '../contracts/events.js';
4
4
  import { activationStream } from '../contracts/streams.js';
5
+ export class ActivationClaimConflictError extends Error {
6
+ constructor(activationId) {
7
+ super(`Activation ${activationId} already has an active Run claim`);
8
+ this.name = 'ActivationClaimConflictError';
9
+ }
10
+ }
5
11
  export async function claimActivation(input) {
6
12
  const { journal, clock, config, activationId, runId, owner, occurredAt } = input;
7
13
  const stream = activationStream(activationId);
8
14
  const events = (await journal.readStream(stream)).map(decodeActivationExecutionEvent);
9
15
  const last = events.at(-1);
10
16
  if (last?.eventType === ExecutionEventType.ActivationClaimed && claimIsUnexpired(last, clock))
11
- throw new Error(`Activation ${activationId} already has an active Run claim`);
17
+ throw new ActivationClaimConflictError(activationId);
12
18
  await journal.append(stream, events.length, [
13
19
  createActivationExecutionEventDraft({
14
20
  eventId: `${activationId}:claim:${runId}`,
@@ -1,5 +1,6 @@
1
1
  export * from './application/execution-projection.js';
2
2
  export * from './application/execution-service.js';
3
+ export * from './application/activation-claim.js';
3
4
  export * from './application/recovery-service.js';
4
5
  export * from './application/run-repository.js';
5
6
  export * from './contracts/commands.js';
@@ -5,10 +5,12 @@ const maximumOwnerRecords = 1024;
5
5
  const compatibilityAcquiredAt = '9999-12-31T23:59:59.999Z';
6
6
  export async function acquireFileLock(path, options) {
7
7
  await mkdir(dirname(path), { recursive: true });
8
+ const identity = await processIdentity(process.pid, options);
8
9
  const metadata = {
9
10
  pid: process.pid,
10
11
  acquiredAt: (options?.now ?? new Date()).toISOString(),
11
12
  lockId: randomUUID(),
13
+ ...(identity === null ? {} : { processIdentity: identity }),
12
14
  };
13
15
  return options?.staleRequiresDeadProcess
14
16
  ? acquireStrictFileLock(path, metadata, options)
@@ -226,10 +228,17 @@ async function ownerBlocksAcquisition(path, options) {
226
228
  return true;
227
229
  return ownerMetadataBlocks(owner, options);
228
230
  }
229
- function ownerMetadataBlocks(owner, options) {
231
+ async function ownerMetadataBlocks(owner, options) {
230
232
  if (options?.staleAfterMs === undefined || !isStale(owner, options))
231
233
  return true;
232
- return Boolean(options.staleRequiresDeadProcess && ownerMayBeAlive(options, owner.pid));
234
+ if (!options.staleRequiresDeadProcess || !ownerMayBeAlive(options, owner.pid))
235
+ return false;
236
+ const identity = await processIdentity(owner.pid, options);
237
+ if (owner.processIdentity !== undefined)
238
+ return identity === null || identity === owner.processIdentity;
239
+ const startedAt = await processStartedAt(owner.pid, options);
240
+ return (startedAt === null ||
241
+ startedAt.getTime() <= Date.parse(owner.acquiredAt) + processStartToleranceMilliseconds);
233
242
  }
234
243
  function ownerRecordName(metadata) {
235
244
  return `${metadata.pid}-${Date.parse(metadata.acquiredAt)}-${metadata.lockId}.json`;
@@ -257,6 +266,57 @@ function ownerMayBeAlive(options, pid) {
257
266
  return true;
258
267
  }
259
268
  }
269
+ const processStartToleranceMilliseconds = 1_000;
270
+ async function processIdentity(pid, options) {
271
+ if (options?.processIdentity !== undefined)
272
+ return options.processIdentity(pid);
273
+ if (process.platform !== 'linux')
274
+ return null;
275
+ try {
276
+ return `linux-start-ticks:${await linuxProcessStartTicks(pid)}`;
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
282
+ async function processStartedAt(pid, options) {
283
+ if (options.processStartedAt !== undefined)
284
+ return options.processStartedAt(pid);
285
+ if (process.platform !== 'linux')
286
+ return null;
287
+ try {
288
+ const [targetTicks, selfTicks, procStat] = await Promise.all([
289
+ linuxProcessStartTicks(pid),
290
+ linuxProcessStartTicks(process.pid),
291
+ readFile('/proc/stat', 'utf8'),
292
+ ]);
293
+ const bootSeconds = /^btime (\d+)$/m.exec(procStat)?.[1];
294
+ if (bootSeconds === undefined)
295
+ return null;
296
+ const currentProcessStartedAt = Date.now() - process.uptime() * 1_000;
297
+ const elapsedSinceBootMilliseconds = currentProcessStartedAt - Number(bootSeconds) * 1_000;
298
+ if (elapsedSinceBootMilliseconds <= 0)
299
+ return null;
300
+ const ticksPerMillisecond = selfTicks / elapsedSinceBootMilliseconds;
301
+ if (!Number.isFinite(ticksPerMillisecond) || ticksPerMillisecond <= 0)
302
+ return null;
303
+ return new Date(Number(bootSeconds) * 1_000 + targetTicks / ticksPerMillisecond);
304
+ }
305
+ catch {
306
+ return null;
307
+ }
308
+ }
309
+ async function linuxProcessStartTicks(pid) {
310
+ const raw = await readFile(`/proc/${pid}/stat`, 'utf8');
311
+ const fields = raw
312
+ .slice(raw.lastIndexOf(')') + 2)
313
+ .trim()
314
+ .split(/\s+/);
315
+ const startTicks = Number(fields[19]);
316
+ if (!Number.isSafeInteger(startTicks) || startTicks < 0)
317
+ throw new Error('Invalid process start ticks');
318
+ return startTicks;
319
+ }
260
320
  export async function withFileLock(path, operation) {
261
321
  const lock = await acquireFileLock(path, {
262
322
  staleAfterMs: 60_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.53",
3
+ "version": "0.3.55",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {