@felan-ai/daemon 0.6.0 → 0.7.1

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 (44) hide show
  1. package/dist/daemon/host-workers.d.ts +34 -0
  2. package/dist/daemon/host-workers.d.ts.map +1 -0
  3. package/dist/daemon/host-workers.js +160 -0
  4. package/dist/daemon/host-workers.js.map +1 -0
  5. package/dist/daemon/maintenance.d.ts +21 -0
  6. package/dist/daemon/maintenance.d.ts.map +1 -0
  7. package/dist/daemon/maintenance.js +48 -0
  8. package/dist/daemon/maintenance.js.map +1 -0
  9. package/dist/daemon/profile-storage.d.ts +3 -1
  10. package/dist/daemon/profile-storage.d.ts.map +1 -1
  11. package/dist/daemon/profile-storage.js +10 -7
  12. package/dist/daemon/profile-storage.js.map +1 -1
  13. package/dist/daemon/projection.d.ts +1 -0
  14. package/dist/daemon/projection.d.ts.map +1 -1
  15. package/dist/daemon/projection.js +12 -5
  16. package/dist/daemon/projection.js.map +1 -1
  17. package/dist/daemon/provider-loader.d.ts +3 -0
  18. package/dist/daemon/provider-loader.d.ts.map +1 -1
  19. package/dist/daemon/provider-loader.js +14 -2
  20. package/dist/daemon/provider-loader.js.map +1 -1
  21. package/dist/daemon/registry.d.ts +6 -0
  22. package/dist/daemon/registry.d.ts.map +1 -1
  23. package/dist/daemon/registry.js +46 -21
  24. package/dist/daemon/registry.js.map +1 -1
  25. package/dist/daemon/server.d.ts +2 -1
  26. package/dist/daemon/server.d.ts.map +1 -1
  27. package/dist/daemon/server.js +2 -2
  28. package/dist/daemon/server.js.map +1 -1
  29. package/dist/daemon/service.d.ts.map +1 -1
  30. package/dist/daemon/service.js +8 -1
  31. package/dist/daemon/service.js.map +1 -1
  32. package/dist/daemon/session-lifecycle-contracts.d.ts +1 -0
  33. package/dist/daemon/session-lifecycle-contracts.d.ts.map +1 -1
  34. package/dist/daemon/session-lifecycle.js +1 -1
  35. package/dist/daemon/session-lifecycle.js.map +1 -1
  36. package/dist/daemon/session-services.d.ts +2 -1
  37. package/dist/daemon/session-services.d.ts.map +1 -1
  38. package/dist/daemon/session-services.js +58 -41
  39. package/dist/daemon/session-services.js.map +1 -1
  40. package/dist/daemon/worker.d.ts +9 -1
  41. package/dist/daemon/worker.d.ts.map +1 -1
  42. package/dist/daemon/worker.js +92 -13
  43. package/dist/daemon/worker.js.map +1 -1
  44. package/package.json +2 -2
@@ -14,6 +14,8 @@ import { isConversationEntry, prepareTranscriptDeletion } from './session-files.
14
14
  import { canonicalProspectivePath, pathInside } from './target.js';
15
15
  import { sessionTip } from './session-cursor.js';
16
16
  import { shutdownStep } from './shutdown-error.js';
17
+ import { HostWorkers } from './host-workers.js';
18
+ import { assertStagedWorker } from './worker.js';
17
19
  const nonRunMutations = new Set(['set_model', 'set_thinking', 'session.rename', 'session.entry.label', 'session.branch.select', 'session.export', 'command.invoke', 'shell.execute', 'processes.write', 'processes.stop', 'steer', 'follow_up', 'queue.clear', 'executions.steer', 'executions.cancel', 'ui.action']);
18
20
  class RegistryOpeningCancelled extends Error {
19
21
  }
@@ -24,6 +26,7 @@ export class SessionRegistry {
24
26
  entries = new Map();
25
27
  executionManagers = new Map();
26
28
  runtimeOpening = new AsyncLocalStorage();
29
+ hostWorkers;
27
30
  bindingActivation = new AsyncLocalStorage();
28
31
  opening = new Map();
29
32
  pathClaims = new Set();
@@ -62,13 +65,25 @@ export class SessionRegistry {
62
65
  this.recovery = options.store.needsRecovery ? options.store.recover().value : undefined;
63
66
  this.processes = new RegistryProcesses({ ...(options.processAdapter ? { processAdapter: options.processAdapter } : {}), store: options.store, now: this.now, assertCurrent: () => options.owner.assertCurrent(),
64
67
  changed: (id, process, rev) => this.publish({ v: 1, session: id, rev, change: { type: 'process.updated', process } }) });
68
+ this.hostWorkers = new HostWorkers({ owner: options.owner, processes: this.processes, lifecycle: () => this.options.lifecycle,
69
+ assertNewWork: () => this.assertHostWork(), fail: error => this.fence(error),
70
+ claim: (id, path) => {
71
+ if (this.options.store.findSessionMetadata(id) || this.hostWorkers.owns(id))
72
+ throw registryError('invalid_request', 'Worker transcript must have a fresh identity.');
73
+ if (this.pathClaims.has(path) || this.reservations.has(path) || this.copyClaims.has(path) || this.list().some(row => row.piPath === path))
74
+ throw registryError('busy', 'Session transcript already has a writer.');
75
+ this.pathClaims.add(path);
76
+ },
77
+ release: path => this.pathClaims.delete(path),
78
+ openSession: request => this.runtimeOpening.run({ workerSession: request.worker.sessionManager.getSessionId() }, () => this.options.lifecycle.openSession(request)),
79
+ });
65
80
  this.lossUnsubscribe = options.owner.onLost(error => { this.fence(error); });
66
81
  }
67
82
  wrapOpeningRuntime(runtime, root) {
68
83
  const opening = this.runtimeOpening.getStore();
69
84
  if (!opening)
70
85
  throw new Error('Runtime factories must run inside lifecycle.openSession.');
71
- const scope = { session: '', root };
86
+ const scope = { session: opening.workerSession ?? '', root };
72
87
  opening.scope = scope;
73
88
  return this.processes.wrap(runtime, scope);
74
89
  }
@@ -104,6 +119,16 @@ export class SessionRegistry {
104
119
  if (this.draining)
105
120
  throw registryError('busy', 'Daemon is draining.');
106
121
  }
122
+ assertHostWork() {
123
+ try {
124
+ this.assertNewWork();
125
+ }
126
+ catch (error) {
127
+ if (error instanceof Error && (this.failure || this.stopping || this.draining))
128
+ error.name = 'AbortError';
129
+ throw error;
130
+ }
131
+ }
107
132
  failExecution(error) { this.fence(error); }
108
133
  get fenced() { return this.failure !== undefined; }
109
134
  beginDrain() {
@@ -114,6 +139,7 @@ export class SessionRegistry {
114
139
  }
115
140
  this.assertAdmission();
116
141
  this.draining = true;
142
+ this.hostWorkers.interrupt();
117
143
  }
118
144
  /** Reopens admission after an abandoned drain; a registry that started shutting down cannot resume. */
119
145
  endDrain() {
@@ -128,6 +154,7 @@ export class SessionRegistry {
128
154
  signal?.throwIfAborted();
129
155
  await this.join([...this.entries.keys()].map(id => this.cancelViews(id)));
130
156
  await this.join([...this.opening.values(), ...this.closes.values(), ...this.lifecycleWriters], undefined, true);
157
+ await this.hostWorkers.settle();
131
158
  for (const entry of this.entries.values()) {
132
159
  await entry.host?.settle();
133
160
  await entry.cancelling;
@@ -175,20 +202,7 @@ export class SessionRegistry {
175
202
  const owner = this.options.store.sessionMetadata(parent);
176
203
  const root = this.require(owner.root).binding;
177
204
  const manager = worker.sessionManager;
178
- const file = manager.getSessionFile();
179
- if (!file || this.list().some(row => row.id === manager.getSessionId()))
180
- throw registryError('invalid_request', 'Worker transcript must have a fresh identity.');
181
- const path = this.options.owner.assertOwnsPath(file);
182
- if (!pathInside(root.resources.sessionDir, path))
183
- throw registryError('forbidden', 'Worker transcript is outside the root session directory.');
184
- const workspace = this.options.owner.assertOwnsPath(worker.runtime.cwd);
185
- if (!(root.resources.workerRoots ?? []).some(directory => pathInside(directory, workspace))) {
186
- throw registryError('forbidden', 'Worker workspace is outside managed staging storage.');
187
- }
188
- const tools = new Set(['read', 'ls', 'grep', 'edit', 'write', ...(worker.customTools ?? []).map(tool => tool.name)]);
189
- if (!worker.allowedTools.length || worker.allowedTools.some(name => !tools.has(name) || name === 'bash')) {
190
- throw registryError('forbidden', 'Worker tools exceed the staged-artifact policy.');
191
- }
205
+ const { path } = assertStagedWorker(this.options.owner, root.resources, worker, id => this.hostWorkers.owns(id) || this.list().some(row => row.id === id));
192
206
  const key = randomUUID();
193
207
  const opening = this.open({ cwd: manager.getCwd(), sessionDir: dirname(path), target: { kind: 'resume', sessionFile: path },
194
208
  parentSessionId: parent, rootSessionId: owner.root, policy: this.getPolicy(parent),
@@ -201,7 +215,12 @@ export class SessionRegistry {
201
215
  this.opening.delete(key);
202
216
  }
203
217
  }
218
+ openHostWorker(worker) { return this.hostWorkers.open(worker); }
219
+ hostWorkSignal() { this.assertHostWork(); return this.hostWorkers.signal; }
220
+ closeHostWorker(id) { return this.hostWorkers.close(id); }
204
221
  async runWorker(id, operation) {
222
+ if (this.hostWorkers.owns(id))
223
+ return this.hostWorkers.run(id, operation);
205
224
  this.assertNewWork();
206
225
  this.assertDispatch(id);
207
226
  const entry = this.require(id);
@@ -290,7 +309,7 @@ export class SessionRegistry {
290
309
  const current = this.options.store.sessionMetadatas();
291
310
  const ids = new Set(current.map(session => session.id));
292
311
  const paths = new Set(current.map(session => session.piPath));
293
- return rows.filter(row => !ids.has(row.id) && !paths.has(row.path) && (cwd === undefined || row.cwd === cwd));
312
+ return rows.filter(row => !ids.has(row.id) && !this.hostWorkers.owns(row.id) && !paths.has(row.path) && (cwd === undefined || row.cwd === cwd));
294
313
  }
295
314
  legacySummary(row) { return this.legacy.summary(row); }
296
315
  /** Reads the titles of listed pre-daemon transcripts, newest first, so the first listing rarely waits for them. */
@@ -1002,6 +1021,8 @@ export class SessionRegistry {
1002
1021
  }
1003
1022
  close(id) {
1004
1023
  this.options.owner.assertCurrent();
1024
+ if (this.hostWorkers.owns(id))
1025
+ return this.closeHostWorker(id);
1005
1026
  const stored = this.options.store.sessionMetadata(id);
1006
1027
  const pending = this.closes.get(id);
1007
1028
  if (pending)
@@ -1060,6 +1081,7 @@ export class SessionRegistry {
1060
1081
  }
1061
1082
  }
1062
1083
  async shutdown(options = {}) {
1084
+ this.options.onShutdown?.();
1063
1085
  if (options.interrupt)
1064
1086
  for (const row of this.list()) {
1065
1087
  if (row.state === 'open' && ['running', 'waiting', 'cancelling'].includes(row.run.state) && row.run.id)
@@ -1069,6 +1091,7 @@ export class SessionRegistry {
1069
1091
  if (this.failure) {
1070
1092
  await shutdownStep('registry-fenced', async () => { await this.failureSettlement; throw new Error('Registry is fenced; verify settlement before releasing ownership.'); });
1071
1093
  }
1094
+ const hostWorkers = shutdownStep('session-dispose', () => this.hostWorkers.shutdown());
1072
1095
  const cancellations = [...this.entries].map(([id, entry]) => {
1073
1096
  if (!options.interrupt)
1074
1097
  return shutdownStep('session-cancel-views', () => this.cancelViews(id), id);
@@ -1086,7 +1109,7 @@ export class SessionRegistry {
1086
1109
  if (!options.interrupt || this.failure)
1087
1110
  throw error;
1088
1111
  });
1089
- await this.join([...cancellations, shutdownStep('registry-pending', () => this.join([...this.opening.values(), closing, ...this.lifecycleWriters], undefined, true))]);
1112
+ await this.join([hostWorkers, ...cancellations, shutdownStep('registry-pending', () => this.join([...this.opening.values(), closing, ...this.lifecycleWriters], undefined, true))]);
1090
1113
  if (this.failure)
1091
1114
  await shutdownStep('registry-fenced', async () => { await this.failureSettlement; throw this.failure; });
1092
1115
  if (options.interrupt) {
@@ -1239,7 +1262,7 @@ export class SessionRegistry {
1239
1262
  } };
1240
1263
  try {
1241
1264
  const { execution = restored?.execution, ...sessionRequest } = request;
1242
- binding = await this.runtimeOpening.run({}, () => this.options.lifecycle.openSession({ ...sessionRequest, resources, ownership, policy: initialPolicy, services,
1265
+ binding = await this.runtimeOpening.run(request.worker ? { workerSession: request.worker.sessionManager.getSessionId() } : {}, () => this.options.lifecycle.openSession({ ...sessionRequest, resources, ownership, policy: initialPolicy, services,
1243
1266
  ...(execution ? { execution: { ...(execution.request.label === undefined ? {} : { label: execution.request.label }) } } : {}),
1244
1267
  }));
1245
1268
  this.assertAdmission();
@@ -1292,7 +1315,7 @@ export class SessionRegistry {
1292
1315
  const path = this.options.owner.assertOwnsPath(actualPath);
1293
1316
  if (restored && (binding.sessionId !== restored.id || path !== restored.piPath || binding.rootSessionId !== restored.root))
1294
1317
  throw new Error('Restored session identity changed.');
1295
- if (this.entries.has(binding.sessionId) || (this.pathClaims.has(path) && claim !== path))
1318
+ if (this.entries.has(binding.sessionId) || this.hostWorkers.owns(binding.sessionId) || (this.pathClaims.has(path) && claim !== path))
1296
1319
  throw new Error('Session already has a live writer.');
1297
1320
  if (claim && claim !== path)
1298
1321
  this.pathClaims.delete(claim);
@@ -1686,6 +1709,7 @@ export class SessionRegistry {
1686
1709
  this.diagnostic({ scope: 'registry', error });
1687
1710
  this.failure = error;
1688
1711
  this.processes.sealAll();
1712
+ this.hostWorkers.interrupt();
1689
1713
  for (const entry of this.entries.values()) {
1690
1714
  entry.runAbort?.abort();
1691
1715
  entry.projection.dispose();
@@ -1698,6 +1722,7 @@ export class SessionRegistry {
1698
1722
  await this.failureSettlement;
1699
1723
  else {
1700
1724
  const failures = [];
1725
+ await this.join([this.hostWorkers.settle()], failures);
1701
1726
  do {
1702
1727
  await this.join(this.pendingWriters(), failures);
1703
1728
  } while (this.hasPendingWriters());
@@ -1710,8 +1735,8 @@ export class SessionRegistry {
1710
1735
  }
1711
1736
  async settleAfterLoss() {
1712
1737
  const failures = [];
1713
- const roots = new Set([...this.entries.values()].map(entry => entry.binding.rootSessionId).concat([...this.processes.liveRoots()]));
1714
- const pending = [...this.opening.values(), ...this.closes.values(), ...[...roots].map(root => this.processes.settle(root, true))];
1738
+ const roots = new Set([...this.entries.values()].map(entry => entry.binding.rootSessionId).concat(this.hostWorkers.roots(), [...this.processes.liveRoots()]));
1739
+ const pending = [this.hostWorkers.settle(), ...this.opening.values(), ...this.closes.values(), ...[...roots].map(root => this.processes.settle(root, true))];
1715
1740
  for (const entry of this.entries.values()) {
1716
1741
  pending.push(entry.binding.runtime.session.abort(), ...(entry.disposing ? [entry.disposing] : []), ...(entry.cancelling ? [entry.cancelling] : []), ...(entry.projectionCancellation ? [entry.projectionCancellation] : []));
1717
1742
  if (entry.host)