@north-light/cli 0.3.579 → 0.3.581
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.
- package/dist/api/client.d.ts +6 -1
- package/dist/api/client.js +3 -3
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/execution-start.d.ts +14 -0
- package/dist/api/dto/health.d.ts +20 -0
- package/dist/api/dto/imports.d.ts +142 -0
- package/dist/api/dto/imports.js +0 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +1 -1
- package/dist/builtin-memory/internal/storage-tiers.md +2 -2
- package/dist/clients/attach/session/connection.d.ts +3 -1
- package/dist/clients/attach/session/connection.js +1 -1
- package/dist/clients/attach/viewer.js +510 -510
- package/dist/commands/api-client.d.ts +7 -4
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/commands/sys/import/resume.d.ts +1 -0
- package/dist/commands/sys/import/resume.js +1 -0
- package/dist/commands/sys/import/start.d.ts +5 -0
- package/dist/commands/sys/import/start.js +4 -0
- package/dist/commands/sys/join.d.ts +2 -0
- package/dist/commands/sys/join.js +1 -1
- package/dist/commands/sys/machine/rotate.d.ts +1 -0
- package/dist/commands/sys/machine/rotate.js +1 -0
- package/dist/commands/sys/signin.d.ts +7 -0
- package/dist/commands/sys/signin.js +2 -0
- package/dist/commands/sys.js +1 -1
- package/dist/core/blobs/r2.d.ts +4 -0
- package/dist/core/blobs/r2.js +7 -4
- package/dist/core/canvas/daemon-owner.d.ts +1 -1
- package/dist/core/canvas/executions.d.ts +1 -1
- package/dist/core/canvas/export-sqlite.js +1 -1
- package/dist/core/canvas/node-tokens.d.ts +2 -0
- package/dist/core/canvas/node-tokens.js +3 -3
- package/dist/core/canvas/schema-pg/009-bodies.sql +10 -0
- package/dist/core/canvas/schema-version.d.ts +1 -1
- package/dist/core/canvas/schema-version.js +1 -1
- package/dist/core/directory/person-credential.d.ts +36 -0
- package/dist/core/directory/person-credential.js +1 -0
- package/dist/core/feed/feed.js +3 -3
- package/dist/core/feed/inbox.js +1 -1
- package/dist/core/feed/reports.js +2 -2
- package/dist/core/graph/access.d.ts +1 -1
- package/dist/core/graph/bodies-boot-copy.d.ts +14 -0
- package/dist/core/graph/bodies-boot-copy.js +1 -0
- package/dist/core/graph/bodies.d.ts +13 -6
- package/dist/core/graph/bodies.js +1 -1
- package/dist/core/graph/deletion.d.ts +4 -1
- package/dist/core/graph/deletion.js +3 -3
- package/dist/core/graph/documents.d.ts +4 -0
- package/dist/core/graph/documents.js +10 -10
- package/dist/core/graph/objects.js +11 -11
- package/dist/core/graph/package-docs.js +5 -5
- package/dist/core/graph/reader.js +2 -2
- package/dist/core/graph/repo-sync/exchange.js +2 -2
- package/dist/core/graph/repo-sync/ref-format.d.ts +0 -1
- package/dist/core/graph/repo-sync/ref-format.js +2 -2
- package/dist/core/import/bundle/blob-keys.d.ts +10 -0
- package/dist/core/import/bundle/blob-keys.js +1 -0
- package/dist/core/import/bundle/layout.d.ts +89 -0
- package/dist/core/import/bundle/layout.js +5 -0
- package/dist/core/import/export/archive.d.ts +11 -0
- package/dist/core/import/export/archive.js +1 -0
- package/dist/core/import/export/cloud.d.ts +21 -0
- package/dist/core/import/export/cloud.js +1 -0
- package/dist/core/import/export/dump.d.ts +34 -0
- package/dist/core/import/export/dump.js +3 -0
- package/dist/core/import/export/files.d.ts +22 -0
- package/dist/core/import/export/files.js +1 -0
- package/dist/core/import/export/inline.d.ts +3 -0
- package/dist/core/import/export/inline.js +1 -0
- package/dist/core/import/export/source.d.ts +2 -0
- package/dist/core/import/export/source.js +1 -0
- package/dist/core/import/export/stores.d.ts +9 -0
- package/dist/core/import/export/stores.js +1 -0
- package/dist/core/import/load/bind.d.ts +19 -0
- package/dist/core/import/load/bind.js +15 -0
- package/dist/core/import/load/blobs.d.ts +10 -0
- package/dist/core/import/load/blobs.js +1 -0
- package/dist/core/import/load/catalog.d.ts +32 -0
- package/dist/core/import/load/catalog.js +9 -0
- package/dist/core/import/load/copy.d.ts +26 -0
- package/dist/core/import/load/copy.js +6 -0
- package/dist/core/import/load/path-map.d.ts +7 -0
- package/dist/core/import/load/path-map.js +1 -0
- package/dist/core/import/load/prepare.d.ts +37 -0
- package/dist/core/import/load/prepare.js +10 -0
- package/dist/core/import/load/rewrite.d.ts +39 -0
- package/dist/core/import/load/rewrite.js +9 -0
- package/dist/core/import/load/rollback.d.ts +3 -0
- package/dist/core/import/load/rollback.js +6 -0
- package/dist/core/import/load/run.d.ts +22 -0
- package/dist/core/import/load/run.js +3 -0
- package/dist/core/import/load/stage.d.ts +15 -0
- package/dist/core/import/load/stage.js +1 -0
- package/dist/core/import/load/stores.d.ts +24 -0
- package/dist/core/import/load/stores.js +2 -0
- package/dist/core/import/load/verify.d.ts +4 -0
- package/dist/core/import/load/verify.js +11 -0
- package/dist/core/peer/receiver.js +1 -1
- package/dist/core/runtime/broker.js +1 -1
- package/dist/core/runtime/revive.d.ts +5 -2
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/roadmap.js +3 -3
- package/dist/core/runtime/runner/blaxel/runner.js +3 -3
- package/dist/core/runtime/runner/index.d.ts +1 -0
- package/dist/core/runtime/runner/index.js +1 -1
- package/dist/core/runtime/runner/machine/call.d.ts +8 -0
- package/dist/core/runtime/runner/machine/call.js +3 -0
- package/dist/core/runtime/runner/machine/environment.d.ts +1 -0
- package/dist/core/runtime/runner/machine/environment.js +1 -0
- package/dist/core/runtime/runner/machine/executor.d.ts +23 -0
- package/dist/core/runtime/runner/machine/executor.js +1 -0
- package/dist/core/runtime/runner/machine/machine-executor.test.d.ts +1 -0
- package/dist/core/runtime/runner/machine/machine-executor.test.js +4 -0
- package/dist/core/runtime/runner/machine/ops.d.ts +127 -0
- package/dist/core/runtime/runner/machine/ops.js +1 -0
- package/dist/core/runtime/runner/machine/runner.d.ts +12 -0
- package/dist/core/runtime/runner/machine/runner.js +1 -0
- package/dist/core/runtime/runner/types.d.ts +7 -1
- package/dist/core/runtime/warm-pool.d.ts +2 -0
- package/dist/core/runtime/warm-pool.js +1 -1
- package/dist/core/secrets/env-rows.d.ts +1 -0
- package/dist/core/secrets/env-rows.js +1 -1
- package/dist/core/storage/purge.d.ts +6 -0
- package/dist/core/storage/purge.js +4 -0
- package/dist/core/subscription-state.d.ts +1 -0
- package/dist/core/subscription-state.js +1 -1
- package/dist/core/substrate/delivery/corpus.d.ts +2 -0
- package/dist/core/substrate/delivery/corpus.js +3 -3
- package/dist/core/substrate/delivery/deliver.js +3 -3
- package/dist/core/substrate/delivery/render-boot.js +25 -25
- package/dist/core/substrate/delivery/render-event.d.ts +1 -1
- package/dist/core/substrate/delivery/render-event.js +4 -4
- package/dist/core/substrate/delivery/types.d.ts +2 -2
- package/dist/daemon/api/bridge.js +3 -3
- package/dist/daemon/api/document-listener.js +4 -4
- package/dist/daemon/api/handlers/attach.js +1 -1
- package/dist/daemon/api/handlers/crons.js +1 -1
- package/dist/daemon/api/handlers/daemon.js +1 -1
- package/dist/daemon/api/handlers/docs.js +1 -1
- package/dist/daemon/api/handlers/execution-start.js +2 -2
- package/dist/daemon/api/handlers/files.js +1 -1
- package/dist/daemon/api/handlers/imports.d.ts +2 -0
- package/dist/daemon/api/handlers/imports.js +1 -0
- package/dist/daemon/api/handlers/machine-credential.d.ts +2 -0
- package/dist/daemon/api/handlers/machine-credential.js +1 -0
- package/dist/daemon/api/handlers/memory.js +4 -4
- package/dist/daemon/api/handlers/profiles.js +1 -1
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/api/handlers/reports.js +4 -4
- package/dist/daemon/api/handlers/runs.js +1 -1
- package/dist/daemon/api/import-auth.d.ts +6 -0
- package/dist/daemon/api/import-auth.js +1 -0
- package/dist/daemon/api/operations.d.ts +2 -2
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/api/router.d.ts +7 -1
- package/dist/daemon/api/router.js +1 -1
- package/dist/daemon/api/server.js +1 -1
- package/dist/daemon/boot-migrations.js +1 -1
- package/dist/daemon/broker-signals/bus.d.ts +3 -2
- package/dist/daemon/broker-signals/bus.js +1 -1
- package/dist/daemon/cron/armed-context.d.ts +1 -0
- package/dist/daemon/cron/armed-context.js +1 -1
- package/dist/daemon/cron/closed.d.ts +31 -0
- package/dist/daemon/cron/closed.js +1 -0
- package/dist/daemon/cron/ended.js +2 -2
- package/dist/daemon/cron/lease-recovery.js +1 -1
- package/dist/daemon/cron/machine-exec.d.ts +5 -0
- package/dist/daemon/cron/machine-exec.js +1 -0
- package/dist/daemon/cron/machine-fire.d.ts +26 -0
- package/dist/daemon/cron/machine-fire.js +1 -0
- package/dist/daemon/cron/recurrence.d.ts +14 -0
- package/dist/daemon/cron/recurrence.js +1 -0
- package/dist/daemon/cron/run-token.d.ts +7 -0
- package/dist/daemon/cron/run-token.js +1 -0
- package/dist/daemon/cron-run.js +2 -2
- package/dist/daemon/crtrd-cli.js +1 -1
- package/dist/daemon/crtrd.d.ts +2 -2
- package/dist/daemon/crtrd.js +10 -10
- package/dist/daemon/drain.d.ts +25 -0
- package/dist/daemon/drain.js +9 -0
- package/dist/daemon/due-sources.d.ts +1 -1
- package/dist/daemon/due-sources.js +1 -1
- package/dist/daemon/due.js +8 -8
- package/dist/daemon/fleet.js +5 -5
- package/dist/daemon/import-dump-run.d.ts +1 -0
- package/dist/daemon/import-dump-run.js +5 -0
- package/dist/daemon/machine-daemon.d.ts +7 -0
- package/dist/daemon/machine-daemon.js +1 -0
- package/dist/daemon/machine-link/client.d.ts +29 -0
- package/dist/daemon/machine-link/client.js +1 -0
- package/dist/daemon/machine-link/config.d.ts +14 -0
- package/dist/daemon/machine-link/config.js +3 -0
- package/dist/daemon/machine-link/credential.d.ts +7 -0
- package/dist/daemon/machine-link/credential.js +2 -0
- package/dist/daemon/machine-link/forward.d.ts +16 -0
- package/dist/daemon/machine-link/forward.js +1 -0
- package/dist/daemon/machine-link/frames.d.ts +49 -0
- package/dist/daemon/machine-link/frames.js +1 -0
- package/dist/daemon/machine-link/holder.d.ts +73 -0
- package/dist/daemon/machine-link/holder.js +9 -0
- package/dist/daemon/manage.d.ts +19 -3
- package/dist/daemon/manage.js +3 -3
- package/dist/daemon/reconcilers/cron-lane.js +1 -1
- package/dist/daemon/reconcilers/delivery-lane.js +2 -2
- package/dist/daemon/reconcilers/import-lane.d.ts +9 -0
- package/dist/daemon/reconcilers/import-lane.js +1 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/remote-wake.d.ts +3 -0
- package/dist/daemon/reconcilers/node-lifecycle/remote-wake.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
- package/dist/daemon/reconcilers/purge-due-work.d.ts +13 -0
- package/dist/daemon/reconcilers/purge-due-work.js +1 -0
- package/dist/daemon/reconcilers/row-claim.d.ts +1 -0
- package/dist/daemon/reconcilers/row-claim.js +2 -2
- package/dist/daemon/review/finish.js +1 -1
- package/dist/daemon/runs/observer.js +3 -3
- package/dist/migrations/004-canvas-documents/apply.d.ts +2 -4
- package/dist/migrations/004-canvas-documents/apply.js +1 -1
- package/dist/migrations/004-canvas-documents/index.js +2 -2
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -13
- package/scripts/sandbox-start.mjs +185 -2
package/dist/daemon/fleet.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var P=Object.defineProperty;var l=(i,e)=>P(i,"name",{value:e,configurable:!0});import{getNode as w,getRow as R}from"../core/canvas/index.js";import{revokeExitedNodeToken as T}from"../core/canvas/node-tokens.js";import{PROFILE_PAUSE_STOP_REASON as _,absorbProfilePauseStop as N,endExecution as y,hasLaunchingExecution as S,hasProfilePauseStop as F,hasRunningRemoteExecution as b,liveExecutionForNode as g,liveExecutionNodeIds as C,markProfilePauseStop as k,recordExecutionExit as O,takeObservedExit as M}from"../core/canvas/executions.js";import{isProfilePaused as z}from"../core/profiles/manifest.js";import{fullName as p}from"../core/canvas/labels.js";import{isSafeNodeId as o}from"../core/canvas/paths.js";import{hasIdleReleaseIntent as A,transition as v}from"../core/runtime/lifecycle.js";import{publishNotice as L}from"../core/runtime/close.js";import{readFault as B,readInterruptedTurn as D,readTransportPoisonVerdict as W}from"../core/canvas/recovery.js";import{hasPendingWake as q}from"./reconcilers/live-obligation.js";import{clearRespawnPolicy as h,deferRespawnWhileOffline as G,MAX_RESPAWN_ATTEMPTS as H,OFFLINE_RESPAWN_RETRY_MS as Y,recordRespawnPolicy as U,respawnBackoffMs as V}from"./reconcilers/node-lifecycle/respawn-policy.js";import{probeOnline as j}from"../core/runtime/connectivity.js";import{runnerForExecution as K}from"../core/runtime/runner/index.js";import{reviveNode as X}from"../core/runtime/revive.js";import{isStartRefusal as J}from"../core/runtime/start-check.js";import{pushUrgent as m}from"../core/feed/feed.js";import{emitEvent as u}from"../core/events/emit.js";import{brokerThresholdsForDaemon as
|
|
1
|
+
var P=Object.defineProperty;var l=(i,e)=>P(i,"name",{value:e,configurable:!0});import{getNode as w,getRow as R}from"../core/canvas/index.js";import{revokeExitedNodeToken as T}from"../core/canvas/node-tokens.js";import{PROFILE_PAUSE_STOP_REASON as _,absorbProfilePauseStop as N,endExecution as y,hasLaunchingExecution as S,hasProfilePauseStop as F,hasRunningRemoteExecution as b,liveExecutionForNode as g,liveExecutionNodeIds as C,markProfilePauseStop as k,recordExecutionExit as O,takeObservedExit as M}from"../core/canvas/executions.js";import{isProfilePaused as z}from"../core/profiles/manifest.js";import{fullName as p}from"../core/canvas/labels.js";import{isSafeNodeId as o}from"../core/canvas/paths.js";import{hasIdleReleaseIntent as A,transition as v}from"../core/runtime/lifecycle.js";import{publishNotice as L}from"../core/runtime/close.js";import{readFault as B,readInterruptedTurn as D,readTransportPoisonVerdict as W}from"../core/canvas/recovery.js";import{hasPendingWake as q}from"./reconcilers/live-obligation.js";import{clearRespawnPolicy as h,deferRespawnWhileOffline as G,MAX_RESPAWN_ATTEMPTS as H,OFFLINE_RESPAWN_RETRY_MS as Y,recordRespawnPolicy as U,respawnBackoffMs as V}from"./reconcilers/node-lifecycle/respawn-policy.js";import{probeOnline as j}from"../core/runtime/connectivity.js";import{runnerForExecution as K}from"../core/runtime/runner/index.js";import{reviveNode as X}from"../core/runtime/revive.js";import{isStartRefusal as J}from"../core/runtime/start-check.js";import{MachineOfflineError as Q}from"../core/runtime/runner/machine/ops.js";import{pushUrgent as m}from"../core/feed/feed.js";import{emitEvent as u}from"../core/events/emit.js";import{brokerThresholdsForDaemon as Z}from"../core/runtime/fleet.js";import{isRecoveryHeld as I}from"../core/runtime/recovery-hold.js";async function E(i,e){const r=await B(i.node_id),t=r===null?"":`The broker reported a ${r.kind} fault on ${r.link} (${r.op}) before it exited: ${r.message}
|
|
2
2
|
|
|
3
3
|
`,s=e===void 0?"The daemon did not observe an exit status.":`The observed exit status was code ${e.code??"null"}, signal ${e.signal??"null"}.`;return`\u26A0 Spawn failed \u2014 \`${i.name}\` (${i.kind}) never started.
|
|
4
4
|
|
|
@@ -6,16 +6,16 @@ Its pi vehicle exited before the session came up (no pi_session_id was ever reco
|
|
|
6
6
|
|
|
7
7
|
`+t+`Inspect canonical event diagnostics with \`crtr sys logs --node ${i.node_id}\`.
|
|
8
8
|
|
|
9
|
-
If the work still needs doing, re-spawn it.`}l(
|
|
9
|
+
If the work still needs doing, re-spawn it.`}l(E,"bootFailureBody");async function $(i,e){await m(i.node_id,await E(i,e),{from:i.node_id})}l($,"surfaceBootFailure");function x(i){return`\u26A0 Out of memory \u2014 \`${i.name}\` (${i.kind}) was killed because its app's processes together passed the app's memory limit.
|
|
10
10
|
|
|
11
|
-
Every process of the run was stopped. The daemon does not restart it, because it would reach the same limit. Reduce what the run holds in memory, or ask the person to raise the app's limit (\`crtr sys limits set --app ${i.grantee??"app:terminal"} --memory <size>\`), then revive the node.`}l(x,"outOfMemoryBody");async function
|
|
11
|
+
Every process of the run was stopped. The daemon does not restart it, because it would reach the same limit. Reduce what the run holds in memory, or ask the person to raise the app's limit (\`crtr sys limits set --app ${i.grantee??"app:terminal"} --memory <size>\`), then revive the node.`}l(x,"outOfMemoryBody");async function ee(i){const e=await W(i.node_id),r=e===null?"":`
|
|
12
12
|
|
|
13
13
|
Provider rotation observed ${e.consecutivePasses} consecutive route-queue passes with no HTTP response (tried ${e.routes.join(", ")}). Check network and provider transport before reviving.`,t=`\u26A0 Crash loop \u2014 \`${i.name}\` (${i.kind}) kept dying without settling a turn.
|
|
14
14
|
|
|
15
15
|
Its session started successfully, but six consecutive relaunches made no progress, so the daemon stopped retrying.`+r+`
|
|
16
16
|
|
|
17
|
-
Fix the crash cause, then revive the node if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(
|
|
17
|
+
Fix the crash cause, then revive the node if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(ee,"surfaceCrashLoop");async function te(i,e){const r=e instanceof Error?e.message:String(e),t=`\u26A0 Recovery failed \u2014 \`${i.name}\` (${i.kind}) could not be relaunched.
|
|
18
18
|
|
|
19
19
|
The daemon refused this recovery before starting a broker: ${r}
|
|
20
20
|
|
|
21
|
-
The node is now dead. Fix the recorded launch configuration, then revive it if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(
|
|
21
|
+
The node is now dead. Fix the recorded launch configuration, then revive it if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(te,"surfaceRecoveryFailure");function re(i){return i?.cycle_pending!==!0}l(re,"retryResumeMode");function ie(i,e,r=!0){return i===null||i.status==="done"||i.status==="canceled"||i.status==="dead"?"forget":i.intent==="refresh"?"respawn-fresh":A(i)?"dormant":i.lifecycle==="resident"&&!e&&!r&&i.pi_session_id!=null?"release-idle":i.pi_session_id!=null?re(i)?"respawn-resume":"respawn-cycle":e||i.fork_from!=null?"respawn-fresh":"boot-failure"}l(ie,"classifyDeadNode");const se=3e4;class Pe{static{l(this,"DaemonFleet")}#a;#n;#r;#o;#e=new Map;#t=new Set;#l=Number.NEGATIVE_INFINITY;constructor(e){this.#a=e.epoch,this.#n=e.enqueue,this.#r=e.now??Date.now,this.#o=e.probeOnline??(()=>j())}epoch(){return this.#a}register(e,r){if(this.#e.has(e))throw new Error(`fleet: ${e} already has a live broker entry \u2014 refusing a second launch (programming error)`);if(r.pid===null)throw new Error(`fleet: refusing to register ${e} with no pid`);this.#t.delete(e),this.#e.set(e,{pid:r.pid,handle:r})}async has(e){return this.#e.has(e)||await b(e)}async isClaimed(e){return this.#e.has(e)||this.#t.has(e)||await b(e)||await S(e,this.#a)}get(e){return this.#e.get(e)}size(){return this.#e.size}async#c(){const e=await C();for(const r of this.#e.keys())e.add(r);for(const r of this.#t)e.add(r);return e}async occupancy(){return(await this.#c()).size}async reserveSlot(e){if(this.#t.has(e)||this.#e.has(e))return!0;const{automaticReviveCap:r}=Z(),t=await this.#c();if(this.#t.has(e)||this.#e.has(e))return!0;if(t.delete(e),t.size<r)return this.#t.has(e)||this.#t.add(e),!0;const s=this.#r();return s-this.#l>=se&&(this.#l=s,u({level:"warn",event:"broker.capacity.revive_deferred",fields:{live_count:t.size,hard_cap:r}})),!1}releaseReservation(e){this.#t.delete(e)}entries(){return this.#e.entries()}async interruptForProfilePause(e,r=this.#e.get(e)?.pid){if(r===void 0){const t=await g(e);if(t===null||t.runner==="process"||t.state!=="running")return;await k(e),(await K(t.runner).stop(t,{reason:_})).confirmed&&await y(t.executionId,{reason:_});return}await k(e);try{process.kill(r,"SIGTERM")}catch{}}async hasProfilePauseInterruption(e){return await F(e)}forget(e){this.#e.delete(e),this.#t.delete(e)}#i=new Set;async exitsSettled(){for(;this.#i.size>0;)await Promise.allSettled([...this.#i])}async onChildExit(e,r,t){const s=this.#u(e,r,t);this.#i.add(s);try{await s}finally{this.#i.delete(s)}}async#u(e,r,t){const s=t??(await g(e))?.executionId;t&&await T(t),this.#e.delete(e),s!==void 0&&(await O(s,r),await y(s,{reason:"exited"})),this.#n(async()=>{const a=await w(e),n=a?.launched_at==null?Number.NaN:Date.parse(a.launched_at),d=Number.isFinite(n)?Math.max(0,this.#r()-n):null,f=a?.respawn_failures??0;this.#f(e,r,d,f)})}async deliverExit(e,r){await this.onChildExit(e,r)}async applyDeadRowPolicy(e){await this.#h(e,await M(e))}async terminalizeIfStillNonTerminal(e,r,t,s,a){const n=await w(e);return n===null||n.status==="done"||n.status==="canceled"||n.status==="dead"?!1:(await this.#s(e,n,{event:r,reason:s,detail:a,wake:`Child crashed \u2014 ${p(n)} (${e}) could not be relaunched and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`,surface:l(()=>te(n,t),"surface")}),!0)}dispose(){}#f(e,r,t,s){u({level:"info",event:"broker.exit.observed",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},code:r.code,signal:r.signal,uptime_ms:t,consecutive_failures:s}})}async#h(e,r){return this.#d(e,r)}async#d(e,r){const t=await w(e);if(t!==null&&z(t.profile_id))return await h(e),"forget";if(await N(e))return await h(e),"forget";if(await I(e))return"dormant";const s=(await D(e))?.interrupted===!0;if(r?.oomKilled===!0&&t!==null&&t.status!=="done"&&t.status!=="canceled"&&t.status!=="dead")return await h(e),await this.#s(e,t,{event:"broker.out_of_memory",reason:"out_of_memory",detail:{schema:"crtr.node-outcome-detail/v1",message:x(t)},wake:`Child out of memory \u2014 ${p(t)} (${e}) used more memory than its app is allowed and is now dead. It is not restarted automatically. Revive it with \`crtr node lifecycle revive ${e}\` once the work needs less memory or the cap is raised (\`crtr sys limits set\`).`,surface:l(async()=>{await m(e,x(t),{from:e})},"surface")}),"forget";const a=ie(t,s,await q(e));switch(a){case"forget":await h(e);break;case"dormant":await h(e);break;case"release-idle":await h(e),await v(e,"release"),u({level:"info",event:"broker.recovery.released",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e}}});break;case"boot-failure":await this.#s(e,t,{event:"broker.boot.failed",reason:"boot_failed",detail:{schema:"crtr.node-outcome-detail/v1",message:await E(t,r)},wake:`Child crashed \u2014 ${p(t)} (${e}) exited before its session ever came up and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`,surface:l(()=>$(t,r),"surface")});break;case"respawn-fresh":case"respawn-cycle":case"respawn-resume":await this.#p(e,t,a,s);break}return a}async#p(e,r,t,s){const a=await R(e);if(a===null)return;const n=a.respawn_failures??0;if(!await this.#o()){await G(e,n,this.#r()),u({level:"info",event:"broker.respawn.offline_deferred",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},action:t,retry_in_ms:Y,consecutive_failures:n}});return}const d=a.respawn_not_before==null,f=d?await U(a,this.#r()):{failures:n,notBefore:a.respawn_not_before,exhausted:n>=H};if(f.exhausted){const c=`Child crashed \u2014 ${p(r)} (${e}) kept dying without settling a turn (${f.failures} consecutive launches) and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`;await this.#s(e,r,{event:"broker.respawn.exhausted",reason:"crash_looped",detail:{schema:"crtr.node-outcome-detail/v1",message:c,respawn_failures:f.failures},wake:c,...r.pi_session_id!=null?{surface:l(()=>ee(r),"surface")}:{}});return}if(d&&f.notBefore!==null){u({level:"info",event:"broker.respawn.deferred",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},action:t,backoff_ms:V(f.failures),consecutive_failures:f.failures}});return}u({level:"info",event:"broker.respawn.started",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},action:t,consecutive_failures:f.failures}});try{await X(e,{capacity:"freeze",resume:t==="respawn-resume",wakeReason:t==="respawn-resume"&&s?"runtime-restart-abort":void 0,recovery:!0})}catch(c){if(J(c)||c instanceof Q){await h(e),await v(e,"release"),u({level:"warn",event:"node.start.refused",node_id:e,error:c,fields:{enact:"respawn"}});return}u({level:"error",event:"broker.respawn.failed",...o(e)?{node_id:e}:{},error:c,fields:{...o(e)?{}:{affected_node_id:e},action:t}}),await this.terminalizeIfStillNonTerminal(e,"broker.respawn.refused",c,"launch_failed",{schema:"crtr.node-outcome-detail/v1",message:c instanceof Error?c.message:String(c)})}}async#s(e,r,t){u({level:"error",event:t.event,...o(e)?{node_id:e}:{fields:{affected_node_id:e}}}),await v(e,"crash",{reason:t.reason,outcome:{detail:t.detail}});try{await(t.surface??(()=>$(r)))()}catch(s){u({level:"error",event:"broker.boot_failure_surface.failed",...o(e)?{node_id:e}:{fields:{affected_node_id:e}},error:s})}await L(e,"notice.crash",t.wake,{reason:"child-crashed",child:e})}}export{Pe as DaemonFleet,ie as classifyDeadNode,re as retryResumeMode,$ as surfaceBootFailure};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runImportDump(out: string | undefined, kind: string | undefined): Promise<number>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var a=Object.defineProperty;var s=(r,t)=>a(r,"name",{value:t,configurable:!0});import{closeAllDbs as n,openDb as d}from"../core/canvas/db.js";import{chmodSync as m,writeFileSync as p}from"node:fs";import{join as c}from"node:path";import{dumpBodiesPre as f,dumpRows as l}from"../core/import/export/dump.js";import{STORE_FILE as w}from"../core/import/export/cloud.js";import{readStores as u}from"../core/import/export/stores.js";async function v(r,t){try{if(t==="install-id"){const o=await(await d()).prepare("SELECT value FROM canvas_meta WHERE key = 'install_id'").get();if(o===void 0)throw new Error("this canvas database has no install id");return process.stdout.write(`${JSON.stringify({installId:o.value})}
|
|
2
|
+
`),0}if(t==="bodies"&&r!==void 0&&r!=="")return process.stdout.write(`${JSON.stringify(await f(r))}
|
|
3
|
+
`),0;if(r===void 0||t!=="machine"&&t!=="hosted")throw new Error("usage: import-dump <out dir> machine|hosted|bodies|install-id");const e=await l(r,t),i=c(r,w);return p(i,JSON.stringify(await u()),{mode:384}),m(i,384),process.stdout.write(`${JSON.stringify(e)}
|
|
4
|
+
`),0}catch(e){return process.stderr.write(`${e.message}
|
|
5
|
+
`),1}finally{await n()}}s(v,"runImportDump");export{v as runImportDump};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HealthDTO } from '../api/index.js';
|
|
2
|
+
/** `/healthz` of the machine role. The daemon is up and serving whatever the link does, so `ok` stays true (a start waiting for it must not fail on an offline Mac);
|
|
3
|
+
* the link is its own fact, and the phase reads `ready` only while it is up. No brokers are reconciled here: this role holds no database. */
|
|
4
|
+
export declare function machineHealth(linkConnected: boolean): HealthDTO;
|
|
5
|
+
/** Start the machine role and return its teardown. Throws when `runtime/machine.json` or the credential is missing: a machine role
|
|
6
|
+
* with nothing to link to must not look like a running daemon. */
|
|
7
|
+
export declare function runMachineDaemon(): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var v=Object.defineProperty;var c=(e,r)=>v(e,"name",{value:r,configurable:!0});import{chmodSync as S,existsSync as w,mkdirSync as p,unlinkSync as y,writeFileSync as k,rmSync as f}from"node:fs";import{createServer as g}from"node:http";import{dirname as I}from"node:path";import{apiSocketPath as E}from"../core/canvas/paths.js";import{bindDaemonEventSource as M}from"../core/events/source.js";import{emitEvent as x}from"../core/events/emit.js";import{createMachineExecutor as G}from"../core/runtime/runner/machine/executor.js";import{socketDir as P}from"../core/layout.js";import{isDaemonRunning as T,pidfilePath as m,readPidfile as _}from"./pidfile.js";import{cancelMachineCron as b,runMachineCron as C}from"./cron/machine-exec.js";import{startMachineLink as D}from"./machine-link/client.js";import{readMachineConfig as U,readMachineCredential as h}from"./machine-link/config.js";function N(e){return{ok:!0,daemon_up:!0,store_accessible:!1,brokers_reconciled:!1,link_connected:e,startup_phase:e?"ready":"recovering"}}c(N,"machineHealth");async function W(){const e=U();if(e===void 0)throw new Error("crtrd --role machine needs runtime/machine.json: this install has not been imported as a machine");if(h(),T())throw new Error("a crtrd is already running for this home");M(),p(P(),{recursive:!0,mode:493});const r=E();w(r)&&y(r);let a;const t=g((o,i)=>{const l=(o.method??"GET")==="GET"&&(o.url??"/").split("?")[0]==="/healthz";i.writeHead(l?200:404,{"content-type":"application/json; charset=utf-8"}),i.end(JSON.stringify(l?N(a?.connected()===!0):{error:{code:"not_found",message:"a machine-role crtrd serves only /healthz",origin:"runtime"}}))});await new Promise((o,i)=>{t.once("error",i),t.listen(r,()=>{t.off("error",i),S(r,384),o()})}),p(I(m()),{recursive:!0}),k(m(),String(process.pid),"utf8");const n=G({apiUrl:e.apiUrl,environmentId:e.environmentId});a=D({apiUrl:e.apiUrl,environmentId:e.environmentId,credential:h,handlers:{start:n.start,wake:n.wake,stop:n.stop,status:n.status,delete:n.delete,viewer:n.viewer,"cron.run":C,"cron.cancel":b}}),x({level:"info",event:"daemon.machine.started",fields:{environment:e.environmentId}});let u;const d=c(()=>u??=(async()=>{a?.stop(),await new Promise(o=>{t.close(()=>o()),t.closeAllConnections()}),_()===process.pid&&(f(r,{force:!0}),f(m(),{force:!0})),process.off("SIGINT",s),process.off("SIGTERM",s)})(),"teardown"),s=c(()=>{d().then(()=>process.exit(0),()=>process.exit(1))},"onSignal");return process.on("SIGINT",s),process.on("SIGTERM",s),d}c(W,"runMachineDaemon");export{N as machineHealth,W as runMachineDaemon};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
import type { MachineCronCancelArgs, MachineCronCancelResult, MachineCronRunArgs, MachineCronRunResult, MachineOp } from '../../core/runtime/runner/machine/ops.js';
|
|
3
|
+
export declare const LINK_BACKOFF_MIN_MS = 1000;
|
|
4
|
+
export declare const LINK_BACKOFF_MAX_MS = 60000;
|
|
5
|
+
/** Bytes written to a view.sock that its peer has not yet accepted, above which the channel is closed. */
|
|
6
|
+
export declare const VIEW_WRITE_LIMIT_BYTES: number;
|
|
7
|
+
/** One function per op; a machine-role crtrd injects the executor and the cron runner. */
|
|
8
|
+
export type MachineHandlers = {
|
|
9
|
+
[O in MachineOp]?: (args: never, signal: AbortSignal) => Promise<unknown>;
|
|
10
|
+
} & {
|
|
11
|
+
'cron.run'?: (args: MachineCronRunArgs) => Promise<MachineCronRunResult>;
|
|
12
|
+
'cron.cancel'?: (args: MachineCronCancelArgs) => Promise<MachineCronCancelResult>;
|
|
13
|
+
};
|
|
14
|
+
export interface MachineLinkOptions {
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
environmentId: string;
|
|
17
|
+
credential: () => string;
|
|
18
|
+
handlers: MachineHandlers;
|
|
19
|
+
/** Opens a node's view.sock; default `connectViewSocket`. */
|
|
20
|
+
openView?: (node: string) => Socket;
|
|
21
|
+
backoffMinMs?: number;
|
|
22
|
+
backoffMaxMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface MachineLink {
|
|
25
|
+
stop(): void;
|
|
26
|
+
connected(): boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function linkUrl(apiUrl: string, environmentId: string): string;
|
|
29
|
+
export declare function startMachineLink(options: MachineLinkOptions): MachineLink;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var b=Object.defineProperty;var c=(o,d)=>b(o,"name",{value:d,configurable:!0});import{WebSocket as w}from"ws";import{emitEvent as E}from"../../core/events/emit.js";import{connectViewSocket as A}from"../../core/runtime/view-socket.js";import{CLOSE_REVOKED as O,LINK_MAX_PAYLOAD_BYTES as T,parseFrame as x}from"./frames.js";const L=1e3,N=6e4,B=4*1024*1024,C=1024*1024,V=8*1024*1024;function K(o,d){const l=new URL(`/v1/machines/${encodeURIComponent(d)}/link`,o);return l.protocol=l.protocol==="https:"?"wss:":l.protocol==="http:"?"ws:":l.protocol,l.toString()}c(K,"linkUrl");function Y(o){const d=o.backoffMinMs??L,l=o.backoffMaxMs??N,S=o.openView??A;let v=!1,f,s=d,p;const k=c((_,t)=>{E({level:"error",event:_,fields:{environment:o.environmentId},error:t})},"fault");let m;const y=c(()=>{if(v)return;if(m!==void 0){if(o.credential()===m){p=setTimeout(y,s),s=Math.min(s*2,l);return}m=void 0}const _=o.credential(),t=new w(K(o.apiUrl,o.environmentId),{headers:{authorization:`Bearer ${_}`},maxPayload:T});f=t;const i=new Map,h=new Map,a=c(e=>{t.readyState===w.OPEN&&t.send(JSON.stringify(e))},"send"),M=c(async e=>{const n=o.handlers[e.op];if(n===void 0){a({type:"result",call_id:e.call_id,ok:!1,error:`this machine does not execute ${e.op}`});return}const r=new AbortController;t.once("close",()=>r.abort(new Error("the machine link closed")));try{a({type:"result",call_id:e.call_id,ok:!0,value:await n(e.args,r.signal)??null})}catch(u){a({type:"result",call_id:e.call_id,ok:!1,error:u instanceof Error?u.message:String(u)})}},"runCall"),g=c(e=>{const n=S(e.node);i.set(e.channel,n),n.once("connect",()=>a({type:"view.ready",channel:e.channel})),n.on("data",r=>{t.readyState===w.OPEN&&(t.send(JSON.stringify({type:"view.data",channel:e.channel,bytes:r.toString("base64")}),()=>{t.bufferedAmount<C&&n.resume()}),t.bufferedAmount>B&&n.pause())}),n.once("error",r=>{i.delete(e.channel)&&a({type:"view.close",channel:e.channel,error:r.message})}),n.once("close",()=>{i.delete(e.channel)&&a({type:"view.close",channel:e.channel})})},"openChannel"),I=c((e,n)=>{const r=i.get(e);if(r===void 0||r.write(n))return;const u=(h.get(e)??0)+n.length;if(u>V){i.delete(e),h.delete(e),r.destroy(),a({type:"view.close",channel:e,error:"the broker is not draining the viewer channel"});return}h.has(e)||r.once("drain",()=>h.delete(e)),h.set(e,u)},"writeToChannel");t.on("open",()=>{s=d}),t.on("message",e=>{const n=x(e);if(n!==null)try{switch(n.type){case"call":M(n);return;case"view.open":g(n);return;case"view.data":I(n.channel,Buffer.from(n.bytes,"base64"));return;case"view.close":{const r=i.get(n.channel);i.delete(n.channel),r?.destroy();return}default:return}}catch(r){k("daemon.machine_link.frame_failed",r)}}),t.on("error",e=>k("daemon.machine_link.client_error",e)),t.on("close",e=>{for(const n of i.values())n.destroy();i.clear(),f===t&&(f=void 0),!v&&(e===O&&(E({level:"error",event:"daemon.machine_link.revoked",fields:{environment:o.environmentId}}),m=_),p=setTimeout(y,s),s=Math.min(s*2,l))})},"connect");return y(),{stop(){v=!0,p!==void 0&&clearTimeout(p),f?.close()},connected:c(()=>f?.readyState===w.OPEN,"connected")}}c(Y,"startMachineLink");export{N as LINK_BACKOFF_MAX_MS,L as LINK_BACKOFF_MIN_MS,V as VIEW_WRITE_LIMIT_BYTES,K as linkUrl,Y as startMachineLink};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** `directoryUrl` is the directory the person signs in against (`crtr sys signin`). */
|
|
2
|
+
export interface MachineConfig {
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
environmentId: string;
|
|
5
|
+
runId: string;
|
|
6
|
+
directoryUrl: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const machineConfigPath: () => string;
|
|
9
|
+
export declare const machineCredentialPath: () => string;
|
|
10
|
+
/** The machine config, or undefined when this install is not an imported machine. A present but malformed file throws. */
|
|
11
|
+
export declare function readMachineConfig(): MachineConfig | undefined;
|
|
12
|
+
export declare function writeMachineConfig(config: MachineConfig): void;
|
|
13
|
+
export declare function readMachineCredential(): string;
|
|
14
|
+
export declare function writeMachineCredential(secret: string): void;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var p=Object.defineProperty;var e=(n,r)=>p(n,"name",{value:r,configurable:!0});import{chmodSync as u,existsSync as i,mkdirSync as o,readFileSync as c,writeFileSync as a}from"node:fs";import{dirname as d,join as m}from"node:path";import{runtimeRoot as s,secretsDir as h}from"../../core/layout.js";const t=e(()=>m(s(),"machine.json"),"machineConfigPath"),f=e(()=>m(h(),"machine-credential"),"machineCredentialPath");function I(){const n=t();if(!i(n))return;const r=JSON.parse(c(n,"utf8"));if(typeof r.apiUrl!="string"||typeof r.environmentId!="string"||typeof r.runId!="string"||typeof r.directoryUrl!="string")throw new Error(`${n} must hold {apiUrl, environmentId, runId, directoryUrl}`);return{apiUrl:r.apiUrl,environmentId:r.environmentId,runId:r.runId,directoryUrl:r.directoryUrl}}e(I,"readMachineConfig");function U(n){o(d(t()),{recursive:!0}),a(t(),`${JSON.stringify(n,null,2)}
|
|
2
|
+
`)}e(U,"writeMachineConfig");function x(){const n=f();if(!i(n))throw new Error(`no machine credential at ${n}`);return c(n,"utf8").trim()}e(x,"readMachineCredential");function S(n){const r=f();o(d(r),{recursive:!0}),a(r,`${n}
|
|
3
|
+
`,{mode:384}),u(r,384)}e(S,"writeMachineCredential");export{t as machineConfigPath,f as machineCredentialPath,I as readMachineConfig,x as readMachineCredential,U as writeMachineConfig,S as writeMachineCredential};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** True when the environment has an unrevoked credential. */
|
|
2
|
+
export declare function hasMachineCredential(environmentId: string): Promise<boolean>;
|
|
3
|
+
/** Mint (or replace) the environment's credential and return the secret. */
|
|
4
|
+
export declare function mintMachineCredential(environmentId: string): Promise<string>;
|
|
5
|
+
export declare function revokeMachineCredential(environmentId: string): Promise<void>;
|
|
6
|
+
/** True when `token` is the environment's current, unrevoked credential. */
|
|
7
|
+
export declare function verifyMachineCredential(environmentId: string, token: string): Promise<boolean>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var i=Object.defineProperty;var t=(e,n)=>i(e,"name",{value:n,configurable:!0});import{createHash as o,randomBytes as s}from"node:crypto";import{openDb as a}from"../../core/canvas/db.js";const r=t(e=>o("sha256").update(e).digest("hex"),"hash");async function E(e){return await(await a()).prepare("SELECT 1 AS ok FROM machine_credentials WHERE environment = ? AND revoked_at IS NULL").get(e)!==void 0}t(E,"hasMachineCredential");async function _(e){const n=`mc_${s(32).toString("base64url")}`;return await(await a()).prepare(`INSERT INTO machine_credentials (environment, token_hash, issued_at, revoked_at) VALUES (?, ?, ?, NULL)
|
|
2
|
+
ON CONFLICT (environment) DO UPDATE SET token_hash = EXCLUDED.token_hash, issued_at = EXCLUDED.issued_at, revoked_at = NULL`).run(e,r(n),new Date().toISOString()),n}t(_,"mintMachineCredential");async function m(e){await(await a()).prepare("UPDATE machine_credentials SET revoked_at = ? WHERE environment = ? AND revoked_at IS NULL").run(new Date().toISOString(),e)}t(m,"revokeMachineCredential");async function p(e,n){return await(await a()).prepare("SELECT 1 AS ok FROM machine_credentials WHERE environment = ? AND token_hash = ? AND revoked_at IS NULL").get(e,r(n))!==void 0}t(p,"verifyMachineCredential");export{E as hasMachineCredential,_ as mintMachineCredential,m as revokeMachineCredential,p as verifyMachineCredential};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
import type { BrokerViewLink } from '../../core/runtime/view-socket.js';
|
|
4
|
+
import { type MachineLinkRefusal } from './holder.js';
|
|
5
|
+
/** Unsent bytes on a viewer WebSocket above which the channel is closed (a viewer that cannot keep up is dropped, never buffered without bound). */
|
|
6
|
+
export declare const VIEW_BUFFER_LIMIT_BYTES: number;
|
|
7
|
+
/** A viewer channel to a node on a machine: over the link this process holds, or forwarded to the API process that does. */
|
|
8
|
+
export declare function openMachineView(environmentId: string, nodeId: string): BrokerViewLink;
|
|
9
|
+
/** One readiness check: the Mac connected the node's view.sock within `timeoutMs`. */
|
|
10
|
+
export declare function probeMachineView(environmentId: string, nodeId: string, timeoutMs: number): Promise<boolean>;
|
|
11
|
+
/** Wait for the node's broker on the Mac to accept a viewer, retrying while its view.sock refuses; false on the deadline or when the launch exits. */
|
|
12
|
+
export declare function waitForMachineView(environmentId: string, nodeId: string, exited: Promise<unknown> | undefined, waitMs: number): Promise<boolean>;
|
|
13
|
+
/** The environment id of a forwarded viewer upgrade path, or null for any other path. */
|
|
14
|
+
export declare function machineViewEnvironment(req: IncomingMessage): string | null;
|
|
15
|
+
/** The holder's side: authenticate the forwarding API process, open a channel on the link this process holds and pipe it. Returns a refusal for the caller to write, or null once upgraded. */
|
|
16
|
+
export declare function handleMachineViewUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, environmentId: string): MachineLinkRefusal | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var x=Object.defineProperty;var a=(s,e)=>x(s,"name",{value:e,configurable:!0});import{EventEmitter as b}from"node:events";import{createHmac as A,timingSafeEqual as I}from"node:crypto";import{WebSocket as l,WebSocketServer as P}from"ws";import{openDb as R}from"../../core/canvas/db.js";import{loadSecretsKey as k}from"../../core/secrets/key.js";import{LINK_MAX_PAYLOAD_BYTES as _,parseFrame as v}from"./frames.js";import{apiAddress as B,heldLinkOf as g,MachineViewLink as E,UNROUTABLE_PREFIX as O}from"./holder.js";const w=8*1024*1024,T=1013,D=/^\/v1\/machines\/([A-Za-z0-9_-]+)\/view$/,L=new P({noServer:!0,maxPayload:_}),S=a(s=>A("sha256",k().key).update(`machine-view:${s}`).digest("hex"),"forwardToken");function $(s,e){const r=g(s);return r!==null?new E(r,e):new q(s,e)}a($,"openMachineView");class q extends b{static{a(this,"ForwardedViewLink")}environmentId;nodeId;ws;ended=!1;failed=!1;queued=[];queuedBytes=0;constructor(e,r){super(),this.environmentId=e,this.nodeId=r,this.dial().catch(t=>this.finish(t instanceof Error?t.message:String(t)))}get destroyed(){return this.ended}write(e){return this.ended?!1:this.ws?.readyState!==l.OPEN?(this.queuedBytes+=e.length,this.queuedBytes>w?(this.finish("machine view: the holder did not accept the channel before its queue filled"),!1):(this.queued.push(e),!0)):this.ws.bufferedAmount>w?(this.finish("machine view: the holder is not draining the channel"),!1):(this.sendData(e),!0)}destroy(){return this.ended||(this.ws?.readyState===l.OPEN&&this.ws.send(JSON.stringify({type:"view.close",channel:""})),this.finish(void 0)),this}sendData(e){this.ws?.send(JSON.stringify({type:"view.data",channel:"",bytes:Buffer.from(e).toString("base64")}))}async dial(){const e=await(await R()).prepare("SELECT api_address, lease_expires_at FROM machine_links WHERE environment = ?").get(this.environmentId);if(e===void 0||Date.parse(e.lease_expires_at)<=Date.now())throw new Error(`machine_offline: machine ${this.environmentId} has no live link`);if(e.api_address.startsWith(O))throw new Error(`machine_unreachable: the API process holding the link wrote ${e.api_address}; set CRTR_API_ADDRESS or listen on TCP`);if(e.api_address===B())throw new Error(`machine_offline: the link row names this API process (${e.api_address}) but it holds no link`);if(this.ended)return;const r=new URL(`/v1/machines/${encodeURIComponent(this.environmentId)}/view`,e.api_address.includes("://")?e.api_address:`ws://${e.api_address}`);r.protocol=r.protocol==="https:"?"wss:":r.protocol==="http:"?"ws:":r.protocol,r.searchParams.set("node",this.nodeId);const t=new l(r.toString(),{headers:{authorization:`Bearer ${S(this.environmentId)}`},maxPayload:_});this.ws=t,t.on("open",()=>{for(const n of this.queued.splice(0))this.sendData(n)}),t.on("unexpected-response",(n,i)=>{i.resume(),this.finish(i.statusCode===409?"machine_offline: the holder no longer holds the link":`machine view refused by ${e.api_address}: ${i.statusCode}`)}),t.on("message",n=>{const i=v(n);i!==null&&(i.type==="view.ready"?this.emit("connect"):i.type==="view.data"?this.emit("data",Buffer.from(i.bytes,"base64")):i.type==="view.close"&&this.finish(i.error))}),t.on("error",n=>this.finish(n.message)),t.on("close",()=>this.finish("machine view forward closed"))}finish(e){this.ended||(this.ended=!0,this.ws?.close(),e!==void 0&&!this.failed&&(this.failed=!0,this.emit("error",Object.assign(new Error(e),{code:"ECONNRESET"}))),this.emit("close",this.failed))}}function M(s,e,r){return new Promise(t=>{const n=$(s,e),i=setTimeout(()=>{t(!1),n.destroy()},r);n.on("connect",()=>{clearTimeout(i),t(!0),n.destroy()}),n.on("error",()=>{}),n.on("close",()=>{clearTimeout(i),t(!1)})})}a(M,"probeMachineView");async function J(s,e,r,t){const n=Date.now()+t;let i=!1;for(r?.then(()=>{i=!0});Date.now()<=n&&!i;){if(await M(s,e,Math.max(1,Math.min(2e3,n-Date.now()))))return!0;await new Promise(c=>setTimeout(c,250))}return!1}a(J,"waitForMachineView");function Y(s){const e=D.exec(new URL(s.url??"/","http://runtime.local").pathname);return e===null?null:e[1]}a(Y,"machineViewEnvironment");function K(s,e,r,t){const n=s.headers.authorization,i=/^Bearer (\S+)$/.exec(Array.isArray(n)?n[0]??"":n??"")?.[1]??"",c=Buffer.from(S(t)),p=Buffer.from(i);if(p.length!==c.length||!I(p,c))return{status:401,text:"Unauthorized",code:"unauthorized",message:"machine view forward credential refused"};const f=new URL(s.url??"/","http://runtime.local").searchParams.get("node");if(f===null||f==="")return{status:400,text:"Bad Request",code:"bad_request",message:"node is required"};const y=g(t);return y===null?{status:409,text:"Conflict",code:"machine_offline",message:"this API process holds no link for the machine"}:(L.handleUpgrade(s,e,r,d=>{const o=new E(y,f),u=a(h=>{d.readyState===l.OPEN&&d.send(JSON.stringify(h))},"send");o.on("connect",()=>u({type:"view.ready",channel:""})),o.on("data",h=>{if(d.bufferedAmount>w){o.destroy(),d.close(T,"viewer too slow");return}u({type:"view.data",channel:"",bytes:h.toString("base64")})}),o.on("error",h=>u({type:"view.close",channel:"",error:h.message})),o.on("close",()=>d.close()),d.on("message",h=>{const m=v(h);m?.type==="view.data"?o.write(Buffer.from(m.bytes,"base64")):m?.type==="view.close"&&o.destroy()}),d.on("close",()=>o.destroy()),d.on("error",()=>o.destroy())}),null)}a(K,"handleMachineViewUpgrade");export{w as VIEW_BUFFER_LIMIT_BYTES,K as handleMachineViewUpgrade,Y as machineViewEnvironment,$ as openMachineView,M as probeMachineView,J as waitForMachineView};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MachineOp } from '../../core/runtime/runner/machine/ops.js';
|
|
2
|
+
export type MachineFrame =
|
|
3
|
+
/** holder → Mac: run `op`. */
|
|
4
|
+
{
|
|
5
|
+
type: 'call';
|
|
6
|
+
call_id: string;
|
|
7
|
+
op: MachineOp;
|
|
8
|
+
args: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** Mac → holder: the call's outcome. */
|
|
11
|
+
| {
|
|
12
|
+
type: 'result';
|
|
13
|
+
call_id: string;
|
|
14
|
+
ok: true;
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'result';
|
|
18
|
+
call_id: string;
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
}
|
|
22
|
+
/** holder → Mac: open a viewer channel to `node`'s view.sock. */
|
|
23
|
+
| {
|
|
24
|
+
type: 'view.open';
|
|
25
|
+
channel: string;
|
|
26
|
+
node: string;
|
|
27
|
+
}
|
|
28
|
+
/** Mac → holder: the channel's view.sock accepted the connection. */
|
|
29
|
+
| {
|
|
30
|
+
type: 'view.ready';
|
|
31
|
+
channel: string;
|
|
32
|
+
}
|
|
33
|
+
/** Either way: base64 bytes of the view.sock stream. */
|
|
34
|
+
| {
|
|
35
|
+
type: 'view.data';
|
|
36
|
+
channel: string;
|
|
37
|
+
bytes: string;
|
|
38
|
+
}
|
|
39
|
+
/** Either way: the channel ended; `error` when it failed (the Mac could not reach the view.sock). */
|
|
40
|
+
| {
|
|
41
|
+
type: 'view.close';
|
|
42
|
+
channel: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
};
|
|
45
|
+
export declare const LINK_MAX_PAYLOAD_BYTES: number;
|
|
46
|
+
/** WebSocket close codes of the holder: a newer link took the environment's row; the credential is revoked (the Mac does not reconnect with it). */
|
|
47
|
+
export declare const CLOSE_SUPERSEDED = 4001;
|
|
48
|
+
export declare const CLOSE_REVOKED = 4003;
|
|
49
|
+
export declare function parseFrame(data: Buffer | ArrayBuffer | Buffer[]): MachineFrame | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=Object.defineProperty;var l=(r,t)=>o(r,"name",{value:t,configurable:!0});const a=33554432,f=4001,i=4003;function p(r){const t=Array.isArray(r)?Buffer.concat(r):Buffer.isBuffer(r)?r:Buffer.from(new Uint8Array(r));try{const e=JSON.parse(t.toString("utf8"));if(typeof e!="object"||e===null)return null;const n=l((...u)=>u.every(c=>typeof e[c]=="string"),"strings");switch(e.type){case"call":return n("call_id","op")?e:null;case"result":return n("call_id")&&(e.ok===!0||e.ok===!1&&n("error"))?e:null;case"view.open":return n("channel","node")?e:null;case"view.ready":case"view.close":return n("channel")?e:null;case"view.data":return n("channel","bytes")?e:null;default:return null}}catch{return null}}l(p,"parseFrame");export{i as CLOSE_REVOKED,f as CLOSE_SUPERSEDED,a as LINK_MAX_PAYLOAD_BYTES,p as parseFrame};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
import type { Duplex } from 'node:stream';
|
|
4
|
+
import { WebSocket } from 'ws';
|
|
5
|
+
import type { BrokerViewLink } from '../../core/runtime/view-socket.js';
|
|
6
|
+
import { type MachineFrame } from './frames.js';
|
|
7
|
+
export declare const LINK_LEASE_MS = 45000;
|
|
8
|
+
export declare const LINK_RENEW_MS = 15000;
|
|
9
|
+
export declare const LINK_PING_MS = 30000;
|
|
10
|
+
/** Prefix of the address an API process writes when nothing tells it where other API processes can reach it; the viewer forward refuses it. */
|
|
11
|
+
export declare const UNROUTABLE_PREFIX = "unroutable:";
|
|
12
|
+
/** The TCP listener's bound `host:port`, used as this process's address when `CRTR_API_ADDRESS` is not set. */
|
|
13
|
+
export declare const setApiAddress: (address: string) => void;
|
|
14
|
+
/** The address other API processes find this one at, written to `machine_links.api_address`: `CRTR_API_ADDRESS`, else the listener's bound address, else an unroutable marker. */
|
|
15
|
+
export declare const apiAddress: () => string;
|
|
16
|
+
export declare class HeldLink extends EventEmitter {
|
|
17
|
+
readonly environmentId: string;
|
|
18
|
+
readonly generation: number;
|
|
19
|
+
readonly ws: WebSocket;
|
|
20
|
+
private readonly token;
|
|
21
|
+
private readonly timers;
|
|
22
|
+
private unsubscribe;
|
|
23
|
+
private claiming;
|
|
24
|
+
private closed;
|
|
25
|
+
private alive;
|
|
26
|
+
readonly channels: Map<string, MachineViewLink>;
|
|
27
|
+
constructor(environmentId: string, generation: number, ws: WebSocket, token: string);
|
|
28
|
+
start(): void;
|
|
29
|
+
private fault;
|
|
30
|
+
send(frame: MachineFrame): boolean;
|
|
31
|
+
/** Renew the row, my sent calls' leases and `last_seen_at`; a lost generation or a revoked credential closes the link. */
|
|
32
|
+
renew(): Promise<void>;
|
|
33
|
+
/** The credential this link authenticated with is still the environment's current one: rotation or revocation evicts the link. */
|
|
34
|
+
private credentialValid;
|
|
35
|
+
/** Claim this environment's pending calls (fenced by this link's generation) and forward them. */
|
|
36
|
+
claim(): Promise<void>;
|
|
37
|
+
/** `sent` is whether the call was written to the link: a call that was sent may have run, one that was not ran nowhere. */
|
|
38
|
+
private failCall;
|
|
39
|
+
/** This link's generation is still the one in the environment's `machine_links` row. */
|
|
40
|
+
private isCurrentGeneration;
|
|
41
|
+
/** Give a claimed, unsent call back to `pending`. */
|
|
42
|
+
private release;
|
|
43
|
+
/** Settle a call this link holds; a call another holder owns or one already settled is left as it is. A result settles only
|
|
44
|
+
* while this link is the current generation (`requireCurrent`); a failure for a dropped link settles whatever the row now says. */
|
|
45
|
+
private settle;
|
|
46
|
+
private onMessage;
|
|
47
|
+
private teardown;
|
|
48
|
+
}
|
|
49
|
+
/** A viewer channel to one node's view.sock on the Mac, as the relay's broker-side link. */
|
|
50
|
+
export declare class MachineViewLink extends EventEmitter implements BrokerViewLink {
|
|
51
|
+
private readonly link;
|
|
52
|
+
private ended;
|
|
53
|
+
private failed;
|
|
54
|
+
private readonly channel;
|
|
55
|
+
constructor(link: HeldLink, nodeId: string);
|
|
56
|
+
get destroyed(): boolean;
|
|
57
|
+
write(line: string | Buffer): boolean;
|
|
58
|
+
destroy(): this;
|
|
59
|
+
/** The channel is over: `error` names a failure (the relay records a fault), none is a clean end. */
|
|
60
|
+
finish(error: string | undefined): void;
|
|
61
|
+
}
|
|
62
|
+
/** The link this process holds for `environmentId`, or null (no link, or another API process holds it). */
|
|
63
|
+
export declare const heldLinkOf: (environmentId: string) => HeldLink | null;
|
|
64
|
+
/** The environment id of a machine-link upgrade path, or null for any other path. */
|
|
65
|
+
export declare function machineLinkEnvironment(req: IncomingMessage): string | null;
|
|
66
|
+
export interface MachineLinkRefusal {
|
|
67
|
+
status: number;
|
|
68
|
+
text: string;
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
/** Authenticate the machine credential and take over the environment's link. Returns a refusal for the caller to write, or null once upgraded. */
|
|
73
|
+
export declare function handleMachineLinkUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, environmentId: string): Promise<MachineLinkRefusal | null>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var N=Object.defineProperty;var s=(i,e)=>N(i,"name",{value:e,configurable:!0});import{EventEmitter as u}from"node:events";import{hostname as k}from"node:os";import{randomUUID as R}from"node:crypto";import{WebSocket as T,WebSocketServer as I}from"ws";import{openDb as r}from"../../core/canvas/db.js";import{emitEvent as _}from"../../core/events/emit.js";import{brokerSignalBus as L}from"../broker-signals/bus.js";import{verifyMachineCredential as E}from"./credential.js";import{CLOSE_REVOKED as y,CLOSE_SUPERSEDED as p,LINK_MAX_PAYLOAD_BYTES as O,parseFrame as b}from"./frames.js";const U=45e3,C=15e3,x=3e4,M=1e3,w=10,P=new I({noServer:!0,maxPayload:O}),c=new Map,H="unroutable:";let g;const q=s(i=>{g=i},"setApiAddress"),W=s(()=>process.env.CRTR_API_ADDRESS??g??`${H}${k()}:${process.pid}`,"apiAddress"),m=s(()=>new Date(Date.now()+U).toISOString(),"lease");class F extends u{static{s(this,"HeldLink")}environmentId;generation;ws;token;timers=[];unsubscribe;claiming=!1;closed=!1;alive=!0;channels=new Map;constructor(e,t,n,a){super(),this.environmentId=e,this.generation=t,this.ws=n,this.token=a}start(){const{ws:e}=this;e.on("message",t=>{this.onMessage(t).catch(n=>this.fault("daemon.machine_link.frame_failed",n))}),e.on("pong",()=>{this.alive=!0}),e.on("close",()=>{this.teardown().catch(t=>this.fault("daemon.machine_link.teardown_failed",t))}),e.on("error",()=>{}),this.timers.push(setInterval(()=>{this.renew().catch(t=>this.fault("daemon.machine_link.renew_failed",t))},C)),this.timers.push(setInterval(()=>{if(!this.alive){e.terminate();return}this.alive=!1;try{e.ping()}catch{}},x)),this.timers.push(setInterval(()=>{this.claim().catch(t=>this.fault("daemon.machine_link.claim_failed",t))},M)),L.subscribe(t=>{t===this.environmentId&&this.claim().catch(n=>this.fault("daemon.machine_link.claim_failed",n))},{kinds:["machine"],onListenStart:s(()=>{this.claim().catch(t=>this.fault("daemon.machine_link.claim_failed",t))},"onListenStart")}).then(t=>{this.closed?t():this.unsubscribe=t})}fault(e,t){_({level:"error",event:e,fields:{environment:this.environmentId},error:t})}send(e){return this.ws.readyState!==T.OPEN?!1:(this.ws.send(JSON.stringify(e)),!0)}async renew(){if(this.closed)return;const e=await r(),t=m();if((await e.prepare("UPDATE machine_links SET lease_expires_at = ? WHERE environment = ? AND generation = ?").run(t,this.environmentId,this.generation)).changes===0){this.ws.close(p,"superseded");return}await e.prepare("UPDATE machine_calls SET lease_expires_at = ? WHERE environment = ? AND state = 'sent' AND claimed_generation = ?").run(t,this.environmentId,this.generation),await e.prepare("UPDATE environments SET last_seen_at = ? WHERE object = ?").run(new Date().toISOString(),this.environmentId),await this.credentialValid()||this.ws.close(y,"revoked")}async credentialValid(){return await E(this.environmentId,this.token)}async claim(){if(!(this.closed||this.claiming)){this.claiming=!0;try{for(;;){const e=await(await r()).prepare(`UPDATE machine_calls SET state = 'sent', claimed_generation = ?, lease_expires_at = ?
|
|
2
|
+
WHERE call_id IN (SELECT call_id FROM machine_calls WHERE environment = ? AND state = 'pending'
|
|
3
|
+
AND EXISTS (SELECT 1 FROM machine_links WHERE environment = ? AND generation = ?)
|
|
4
|
+
ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT ${w})
|
|
5
|
+
RETURNING call_id, op, args_json`).all(this.generation,m(),this.environmentId,this.environmentId,this.generation),t=!this.closed&&await this.isCurrentGeneration();for(const n of e)t?this.send({type:"call",call_id:n.call_id,op:n.op,args:JSON.parse(n.args_json)})||await this.failCall(n.call_id,"machine_offline","link closed before the call was sent",!1):await this.release(n.call_id);if(e.length<w)return}}finally{this.claiming=!1}}}async failCall(e,t,n,a){await this.settle(e,"failed",{code:t,message:n,sent:a},!1)}async isCurrentGeneration(){return await(await r()).prepare("SELECT 1 AS live FROM machine_links WHERE environment = ? AND generation = ?").get(this.environmentId,this.generation)!==void 0}async release(e){await(await r()).prepare("UPDATE machine_calls SET state = 'pending', claimed_generation = NULL, lease_expires_at = NULL WHERE call_id = ? AND state = 'sent' AND claimed_generation = ?").run(e,this.generation)}async settle(e,t,n,a){await B(e,t,n,this.environmentId,this.generation,a)}async onMessage(e){const t=b(e);if(t!==null)switch(t.type){case"result":t.ok?await this.settle(t.call_id,"done",t.value??null,!0):await this.settle(t.call_id,"failed",{code:"machine_call_failed",message:t.error},!0);return;case"view.ready":this.channels.get(t.channel)?.emit("connect");return;case"view.data":this.channels.get(t.channel)?.emit("data",Buffer.from(t.bytes,"base64"));return;case"view.close":this.channels.get(t.channel)?.finish(t.error);return;default:return}}async teardown(){if(this.closed)return;this.closed=!0;for(const n of this.timers)clearInterval(n);this.unsubscribe?.(),c.get(this.environmentId)===this&&c.delete(this.environmentId);for(const n of[...this.channels.values()])n.finish("machine link closed");const e=await r(),t=await e.prepare("SELECT call_id FROM machine_calls WHERE environment = ? AND state = 'sent' AND claimed_generation = ?").all(this.environmentId,this.generation);for(const{call_id:n}of t)await this.failCall(n,"machine_offline","link dropped before the result",!0);await e.prepare("DELETE FROM machine_links WHERE environment = ? AND generation = ?").run(this.environmentId,this.generation)}}async function B(i,e,t,n,a,l){const o=await r();(await o.prepare(`UPDATE machine_calls SET state = ?, result_json = ?, finished_at = ?
|
|
6
|
+
WHERE call_id = ? AND environment = ? AND claimed_generation = ? AND state = 'sent'
|
|
7
|
+
${l?"AND EXISTS (SELECT 1 FROM machine_links WHERE environment = ? AND generation = ?)":""}`).run(e,JSON.stringify(t),new Date().toISOString(),i,n,a,...l?[n,a]:[])).changes>0&&await o.prepare("SELECT pg_notify(?, ?)").get("crtr_signal",`machine_call:${i}`)}s(B,"withNotify");class ee extends u{static{s(this,"MachineViewLink")}link;ended=!1;failed=!1;channel=R();constructor(e,t){super(),this.link=e,e.channels.set(this.channel,this),e.send({type:"view.open",channel:this.channel,node:t})||queueMicrotask(()=>this.finish("machine link closed"))}get destroyed(){return this.ended}write(e){return!this.ended&&this.link.send({type:"view.data",channel:this.channel,bytes:Buffer.from(e).toString("base64")})}destroy(){return this.ended||(this.link.send({type:"view.close",channel:this.channel}),this.finish(void 0)),this}finish(e){this.ended||(this.ended=!0,this.link.channels.delete(this.channel),e!==void 0&&!this.failed&&(this.failed=!0,this.emit("error",Object.assign(new Error(e),{code:"ECONNRESET"}))),this.emit("close",this.failed))}}const te=s(i=>c.get(i)??null,"heldLinkOf"),K=/^\/v1\/machines\/([A-Za-z0-9_-]+)\/link$/;function ne(i){const e=K.exec(new URL(i.url??"/","http://runtime.local").pathname);return e===null?null:e[1]}s(ne,"machineLinkEnvironment");async function ie(i,e,t,n){const a=i.headers.authorization,l=/^Bearer (\S+)$/.exec(Array.isArray(a)?a[0]??"":a??"")?.[1];if(l===void 0||!await E(n,l))return{status:401,text:"Unauthorized",code:"unauthorized",message:"machine credential refused"};const o=new Date().toISOString(),h=await r(),v=await h.prepare(`INSERT INTO machine_links (environment, generation, api_address, connected_at, lease_expires_at) VALUES (?, 1, ?, ?, ?)
|
|
8
|
+
ON CONFLICT (environment) DO UPDATE SET generation = machine_links.generation + 1, api_address = EXCLUDED.api_address, connected_at = EXCLUDED.connected_at, lease_expires_at = EXCLUDED.lease_expires_at
|
|
9
|
+
RETURNING generation`).get(n,W(),o,m());await h.prepare("UPDATE environments SET last_seen_at = ? WHERE object = ?").run(o,n);const f=Number(v.generation);return e.destroyed?(await h.prepare("DELETE FROM machine_links WHERE environment = ? AND generation = ?").run(n,f),null):(P.handleUpgrade(i,e,t,S=>{const d=new F(n,f,S,l),D=c.get(n);c.set(n,d),D?.ws.close(p,"superseded"),d.start(),d.claim().catch(A=>_({level:"error",event:"daemon.machine_link.claim_failed",fields:{environment:n},error:A}))}),null)}s(ie,"handleMachineLinkUpgrade");export{F as HeldLink,U as LINK_LEASE_MS,x as LINK_PING_MS,C as LINK_RENEW_MS,ee as MachineViewLink,H as UNROUTABLE_PREFIX,W as apiAddress,ie as handleMachineLinkUpgrade,te as heldLinkOf,ne as machineLinkEnvironment,q as setApiAddress};
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HealthDTO, type StartupPhaseDTO } from '../api/index.js';
|
|
2
|
+
import type { DumpResult } from '../core/import/export/dump.js';
|
|
2
3
|
/** Env keys that must NEVER reach the daemon process. Restarting crtrd is
|
|
3
4
|
* overwhelmingly done from inside an agent node's own bash tool (`crtr sys
|
|
4
5
|
* daemon stop && start`), which inherits that node's FULL env — its identity
|
|
@@ -46,10 +47,25 @@ export interface DaemonExportSqliteResult {
|
|
|
46
47
|
ms: number;
|
|
47
48
|
userVersion: number;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
export interface DaemonExportSqliteResult {
|
|
51
|
+
path: string;
|
|
52
|
+
rows: number;
|
|
53
|
+
ms: number;
|
|
54
|
+
userVersion: number;
|
|
55
|
+
}
|
|
56
|
+
/** Run the selected generation's `crtrd-cli export-sqlite` and wait for it. */
|
|
52
57
|
export declare function runDaemonExportSqlite(): Promise<DaemonExportSqliteResult>;
|
|
58
|
+
/** Run the selected generation's `crtrd-cli import-dump`: the row files of the running canvas into `out`/rows. */
|
|
59
|
+
export declare function runDaemonImportDump(out: string, kind: 'machine' | 'hosted'): Promise<DumpResult>;
|
|
60
|
+
/** Run `crtrd-cli import-dump <out> bodies`: every `bodies` row into `out`/rows/bodies (the pre-pause dump; the later `runDaemonImportDump` adds the rows written since). */
|
|
61
|
+
export declare function runDaemonBodiesDump(out: string): Promise<{
|
|
62
|
+
parts: string[];
|
|
63
|
+
count: number;
|
|
64
|
+
}>;
|
|
65
|
+
/** Run `crtrd-cli import-dump <out> install-id`: this install's id, read from the running canvas. */
|
|
66
|
+
export declare function runDaemonInstallId(): Promise<{
|
|
67
|
+
installId: string;
|
|
68
|
+
}>;
|
|
53
69
|
export interface SpawnDaemonOptions {
|
|
54
70
|
/** The phase that proves a daemon launched by this call is available. */
|
|
55
71
|
targetPhase?: 'ready' | 'prepared';
|
package/dist/daemon/manage.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).at(-1)??"";return JSON.parse(
|
|
3
|
-
${r}`),this.name="DaemonExitedBeforeReadyError",this.stderrTail=r}}class
|
|
1
|
+
var X=Object.defineProperty;var o=(e,t)=>X(e,"name",{value:t,configurable:!0});import{spawn as B,execFileSync as q}from"node:child_process";import{createRequire as V}from"node:module";import{dirname as k,join as D}from"node:path";import{fileURLToPath as Q,pathToFileURL as I}from"node:url";import{existsSync as H,lstatSync as Z,mkdirSync as ee,openSync as U,closeSync as j,fstatSync as K,readSync as te}from"node:fs";import{crtrHome as g}from"../core/canvas/paths.js";import{listProcessTable as re}from"../core/canvas/pid.js";import{bindDaemonEventSource as ne,eventSource as oe}from"../core/events/source.js";import{hostExecPath as O}from"../core/runtime/branded-host.js";import{isDaemonRunning as ae,readPidfile as E,isPidAlive as f}from"./pidfile.js";import{DAEMON_STARTUP_WINDOW_MS as G}from"./startup-policy.js";import{exclusiveLockOwnerPid as ie}from"../core/exclusive-lock.js";import{onDiskMigrationLockPath as se}from"../core/canvas/paths.js";import{envNoDaemonAutostart as ce}from"../shared/env.js";import{awaitJoinLockRelease as ue,joinInProgress as le,joinInProgressError as de,joinLockHolder as pe}from"../core/directory/join-lock.js";import{waitForDaemonAvailability as fe}from"../api/index.js";import{localClient as me}from"../api/node-transport.js";const he=["CRTR_NODE_ID","CRTR_KIND","CRTR_MODE","CRTR_LIFECYCLE","CRTR_NODE_CWD","CRTR_CONTEXT_DIR","CRTR_CYCLES","CRTR_PROFILE_ID","CRTR_PARENT_NODE_ID","CRTR_FRONT_DOOR","TMUX","TMUX_PANE","CRTR_BROKER_ENGINE","CRTR_PI_BINARY","NODE_OPTIONS"];function M(){const e={...process.env};for(const t of he)delete e[t];return e}o(M,"sanitizedDaemonEnv");function we(){for(let e=k(Q(import.meta.url));;e=k(e)){if(H(D(e,"bin","runtime-generation-manifest.mjs")))return e;if(k(e)===e)throw new Error(`cannot locate the crouter generation root from ${import.meta.url}`)}}o(we,"generationRoot");async function A(){const e=we(),t=I(D(e,"bin","runtime-generation-manifest.mjs")).href,{runtimeHome:r}=await import(t);try{Z(D(r(),"selected"))}catch(s){if(s.code!=="ENOENT")throw s;return["--conditions","crtr-src","--import",I(V(import.meta.url).resolve("tsx/esm")).href,D(e,"src","daemon","crtrd-cli.ts")]}const n=I(D(e,"bin","runtime-selector.mjs")).href,{tryResolveRuntime:a}=await import(n),c=a("daemon");if(!c.ok)throw new Error(`cannot resolve the selected runtime generation's daemon entry: ${c.error}`);return[c.runtime.entryPath]}o(A,"resolveDaemonLaunch");function _e(e,t){return e.includes(` --canvas-home ${t} `)||e.endsWith(` --canvas-home ${t}`)}o(_e,"daemonCommandDeclaresCanvasHome");function De(){const e=re();if(e===null)return[];const t=g(),r=[];for(const{pid:n,command:a}of e)a.includes("daemon/crtrd")&&_e(a,t)&&n!==process.pid&&r.push(n);return r}o(De,"findDaemonPids");function Ee(e){const t=De().filter(n=>n!==e);for(const n of t)try{process.kill(n,"SIGTERM")}catch{}if(t.length===0)return t;const r=Date.now()+2e3;for(;Date.now()<r&&t.some(n=>f(n));)q("sleep",["0.1"]);for(const n of t)if(f(n))try{process.kill(n,"SIGKILL")}catch{}return t}o(Ee,"sweepStrayDaemons");async function Ve(){const e=process.execve;if(e===void 0)throw new Error(`this Node.js ${process.version} runtime cannot replace PID 1 with the successor daemon`);const t=O(),r=await A();return e(t,[t,...r,"--canvas-home",g()],M())}o(Ve,"execDaemon");async function R(e){const t=await A(),r=B(O(),[...t,...e],{stdio:["ignore","pipe","pipe"],env:M()});let n="",a="";r.stdout.setEncoding("utf8").on("data",u=>{n+=u}),r.stderr.setEncoding("utf8").on("data",u=>{a+=u});const c=await new Promise((u,m)=>{r.once("error",m),r.once("close",p=>u(p))});if(c!==0)throw new Error(a.trim()||`the ${e[0]} process exited with code ${c}`);const s=n.trim().split(`
|
|
2
|
+
`).at(-1)??"";return JSON.parse(s)}o(R,"runDaemonEntry");function Qe(){return R(["export-sqlite"])}o(Qe,"runDaemonExportSqlite");function Ze(e,t){return R(["import-dump",e,t])}o(Ze,"runDaemonImportDump");function et(e){return R(["import-dump",e,"bodies"])}o(et,"runDaemonBodiesDump");function tt(){return R(["import-dump","","install-id"])}o(tt,"runDaemonInstallId");const xe=3*6e4,z=2e4,N=10,P=1e3;async function L(e=P){const t=Math.min(e,P);return me({autostart:!1,timeoutMs:t}).probeHealthz(t)}o(L,"probeDaemonHealth");function ye(e){const t=e instanceof Error?e.message:String(e);if(typeof e!="object"||e===null)return t;const r=e.code,n=e.cause,a=typeof r=="string"?r:n?.code;return typeof a=="string"&&!t.includes(a)?`${t}: ${a}`:t}o(ye,"probeErrorMessage");async function J(e=P){const t=await L(e);if(t.startup_phase==="blocked"){const r=E()??process.pid;throw new v(r,t.startup_blocked??"daemon startup is blocked")}if(!t.ok)throw new Error(t.startup_blocked??"crtrd health check reported not ready")}o(J,"probeDaemonServing");async function rt(){try{return await J(),!0}catch{return!1}}o(rt,"isDaemonServing");function $(e){return new Promise(t=>{setTimeout(t,e)})}o($,"sleepMs");class C extends Error{static{o(this,"DaemonExitedBeforeReadyError")}stderrTail;constructor(t,r){super(r===""?t:`${t}
|
|
3
|
+
${r}`),this.name="DaemonExitedBeforeReadyError",this.stderrTail=r}}class v extends Error{static{o(this,"DaemonStartupBlockedError")}constructor(t,r){super(`daemon ${t} is running but normal startup is blocked: ${r}`),this.name="DaemonStartupBlockedError"}}class F extends Error{static{o(this,"DaemonNotPreparedError")}constructor(t,r){super(r===void 0?`daemon ${t} is serving but did not report the prepared startup phase`:`daemon ${t} reported startup phase ${r}, not prepared`),this.name="DaemonNotPreparedError"}}async function ge(e,t=G,r={},n="ready"){const a=r.readPidfile??E,c=r.isPidAlive??f,s=r.childExited,u=r.isDaemonServing,m=r.probeDaemonServing??(async w=>{if(u!==void 0){if(await u())return;throw new Error("crtrd health check reported not ready")}await J(w)}),p=r.probeDaemonHealth??L,x=r.sleepMs??$,y=r.now??Date.now;let i=null,l=null;return await fe({windowMs:t,pollIntervalMs:N,now:y,sleep:x,retry:o(w=>w!==l,"retry"),probe:o(async w=>{const h=a();if(h===null||!c(h)){const d=s?.();throw d!=null&&(d.signal!==null?l=new C(`daemon ${e} exited before becoming ready (by ${d.signal})`,r.childStderrTail?.()??""):d.code!==0&&(l=new C(`daemon ${e} exited before becoming ready (with exit code ${d.code??"?"})`,r.childStderrTail?.()??"")),l!==null)?l:new Error(`daemon ${e} has no live pidfile owner`)}if(n==="ready"){try{await m(w)}catch(d){throw d instanceof v&&(l=d),d}i=h;return}const _=await p(w);if(_.startup_phase==="prepared"){i=h;return}throw _.startup_phase==="blocked"?(l=new v(h,_.startup_blocked??"on-disk migration is blocked"),l):_.startup_phase==="ready"?(l=new F(h,_.startup_phase),l):new Error(`daemon ${h} is ${_.startup_phase??"not reporting a startup phase"}; waiting for prepared`)},"probe")}),i===e?null:i}o(ge,"verifyDaemonStartup");async function Se(e,t=z,r={}){const n=r.readPidfile??E,a=r.isPidAlive??f,c=r.sleepMs??$,s=r.now??Date.now,u=s()+t;for(;s()<=u;){if(!a(e)&&n()!==e)return;await c(N)}throw new Error(`daemon ${e} did not exit and release its pidfile within ${t}ms`)}o(Se,"waitForDaemonExit");const Re=2e3,ve=5e3;async function Y(e,t=z,r={}){const n=r.isPidAlive??f,a=r.sleepMs??$,c=r.now??Date.now;try{process.kill(e,"SIGTERM")}catch{}try{return await Se(e,t,r),"terminated"}catch{}try{process.kill(e,"SIGKILL")}catch{}const s=c()+Re;for(;c()<=s;){if(!n(e))return"killed";await a(N)}throw new Error(`daemon ${e} survived both SIGTERM and SIGKILL`)}o(Y,"stopDaemonProcess");async function nt(){const e=E(),t=e!==null&&f(e),r=t&&await Y(e)==="killed",n=Ee(null);return{stopped:t||n.length>0,...t?{pid:e}:{},strays_killed:n,force_killed:r}}o(nt,"stopAllDaemons");function Te(e,t,r=2048){let n;try{n=U(e,"r");const{size:a}=K(n),c=Math.max(t,a-r),s=a-c;if(s<=0)return"";const u=Buffer.alloc(s);return te(n,u,0,s,c),u.toString("utf8").trim()}catch{return""}finally{if(n!==void 0)try{j(n)}catch{}}}o(Te,"readLogTailFrom");async function be(e={}){if(process.platform==="linux"&&H("/run/crtr/launcher.sock")&&!f(E()??-1))throw new Error("crtrd is managed by the root launcher; it cannot self-daemonize");const t=e.targetPhase??"ready",r=E();if(r!==null&&f(r))try{const i=await L();if(t==="prepared"){if(i.startup_phase==="prepared")return{started:!1,existing_pid:r,startup_phase:i.startup_phase};throw new F(r,i.startup_phase)}return i.ok?{started:!1,existing_pid:r,...i.startup_phase===void 0?{}:{startup_phase:i.startup_phase}}:{started:!1,existing_pid:r,running:!0,serving:!1,probe_error:i.startup_blocked??"crtrd health check reported not ready",...i.startup_phase===void 0?{}:{startup_phase:i.startup_phase}}}catch(i){if(i instanceof F)throw i;return{started:!1,existing_pid:r,running:!0,serving:!1,probe_error:ye(i)}}ee(g(),{recursive:!0});const n=await A();e.waitForJoinLock===!0&&await ue(xe);const a=pe();if(a!==null&&a!==process.pid&&e.ignoreJoinLock!==!0)throw de(a);const c=D(g(),"crtrd.err"),s=U(c,"a"),u=(()=>{try{return K(s).size}catch{return 0}})();oe()===void 0&&ne();const m=B(O(),[...n,"--canvas-home",g(),...t==="prepared"?["--prepare"]:[]],{detached:!0,stdio:["ignore",s,s],env:M()});j(s);const p=m.pid;if(p===void 0)throw new Error("daemon spawn did not return a pid");let x=null;m.once("exit",(i,l)=>{x={code:i,signal:l}}),m.unref();let y;try{y=await ge(p,G,{childExited:o(()=>x,"childExited"),childStderrTail:o(()=>Te(c,u),"childStderrTail")},t)}catch(i){const l=ie(se())===p;if(x===null&&!l&&!(i instanceof v))try{await Y(p,ve)}catch{}throw i}return y!==null?{started:!1,existing_pid:y,startup_phase:t}:{started:!0,pid:p,startup_phase:t}}o(be,"spawnDaemon");function ke(){return ce()||process.argv.includes("--no-autostart")}o(ke,"autostartDisabled");const Ie=5e3,Oe=6e4;let W=!1,S,T=0,b=0;function Me(e){return Math.min(Ie*2**(e-1),Oe)}o(Me,"autostartBackoffMs");function Ae(){S=void 0,T=0,b=0}o(Ae,"resetDaemonAutostart");function ot(){return S}o(ot,"daemonAutostartStartupFailure");function at(e={}){const t=e.isDaemonRunning??ae,r=e.spawnDaemon??be,n=e.now??Date.now;if(!ke()&&!le()&&!W&&!(n()<b)){if(t()){Ae();return}S=void 0,W=!0,r().then(()=>{S=void 0,T=0,b=0},a=>{a instanceof C&&(S=a.message),T+=1,b=n()+Me(T)}).finally(()=>{W=!1})}}o(at,"ensureDaemon");export{he as DAEMON_ENV_STRIP_KEYS,C as DaemonExitedBeforeReadyError,F as DaemonNotPreparedError,v as DaemonStartupBlockedError,ke as autostartDisabled,ot as daemonAutostartStartupFailure,_e as daemonCommandDeclaresCanvasHome,at as ensureDaemon,Ve as execDaemon,De as findDaemonPids,rt as isDaemonServing,L as probeDaemonHealth,J as probeDaemonServing,ye as probeErrorMessage,Ae as resetDaemonAutostart,et as runDaemonBodiesDump,Qe as runDaemonExportSqlite,Ze as runDaemonImportDump,tt as runDaemonInstallId,M as sanitizedDaemonEnv,be as spawnDaemon,nt as stopAllDaemons,Y as stopDaemonProcess,Ee as sweepStrayDaemons,ge as verifyDaemonStartup,Se as waitForDaemonExit};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var c=Object.defineProperty;var t=(i,e)=>c(i,"name",{value:e,configurable:!0});import{emitEvent as o}from"../../core/events/emit.js";import{operationIdContext as n}from"../../core/events/operation-id.js";import{sweepOrphanCronFolders as l}from"../cron/folders.js";import{reapRebuiltCronExecutions as s}from"../cron/run-socket.js";import{recoverStaleCronLeases as f,runDueCrons as p}from"../cron-run.js";import{isDrained as m}from"../drain.js";import{runThrottledLane as d}from"./throttled-lane.js";class g{static{t(this,"CronLaneReconciler")}async run(e,a){await n.fresh(async()=>{try{await f(e,{register:t(r=>a.lifecycle.registerDetached(r),"register")})}catch(r){o({level:"error",event:"cron.lease_recovery.failed",error:r})}});try{await s()}catch(r){o({level:"error",event:"cron.execution.reap_failed",error:r})}try{await d("cron-folder-sweep","",e,6e4,6e4,async()=>{await l()})}catch(r){o({level:"error",event:"cron.folder.sweep_failed",error:r})}if(a.lifecycle.acceptsDetachedWork()&&!await m())try{await p(e,{register:t(r=>a.lifecycle.registerDetached(r),"register")})}catch(r){o({level:"error",event:"cron.run.failed",error:r})}}}export{g as CronLaneReconciler};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
var g=Object.defineProperty;var p=(e,t)=>g(e,"name",{value:t,configurable:!0});import{randomUUID as D}from"node:crypto";import{claimPushDelivery as h,duePushDeliveries as j,messageBodyForDelivery as N,pushDeliveryClaimIsCurrent as O,settleDelivery as d}from"../../core/canvas/deliveries.js";import{deliveryBackoffMs as f}from"../../core/canvas/delivery-contract.js";import{openDb as c}from"../../core/canvas/db.js";import{pushLineFor as u}from"../../core/graph/custom.js";import{postReceiverUrl as b}from"../../core/graph/receiver-url.js";import{emitEvent as l}from"../../core/events/emit.js";import{operationIdContext as m}from"../../core/events/operation-id.js";import{runDelivery as w,DELIVERY_TIMEOUT_MS as E}from"../delivery/run-delivery.js";const v=D();async function o(e,t,a){await d(e.deliveryId,e.attempts,t,{state:"permanent_failed",failure:{kind:"spawn_error",message:a}},Date.now())}p(o,"failed");async function
|
|
1
|
+
var g=Object.defineProperty;var p=(e,t)=>g(e,"name",{value:t,configurable:!0});import{randomUUID as D}from"node:crypto";import{claimPushDelivery as h,duePushDeliveries as j,messageBodyForDelivery as N,pushDeliveryClaimIsCurrent as O,settleDelivery as d}from"../../core/canvas/deliveries.js";import{deliveryBackoffMs as f}from"../../core/canvas/delivery-contract.js";import{openDb as c}from"../../core/canvas/db.js";import{pushLineFor as u}from"../../core/graph/custom.js";import{postReceiverUrl as b}from"../../core/graph/receiver-url.js";import{emitEvent as l}from"../../core/events/emit.js";import{operationIdContext as m}from"../../core/events/operation-id.js";import{runDelivery as w,DELIVERY_TIMEOUT_MS as E}from"../delivery/run-delivery.js";import{isDrained as T}from"../drain.js";const v=D();async function o(e,t,a){await d(e.deliveryId,e.attempts,t,{state:"permanent_failed",failure:{kind:"spawn_error",message:a}},Date.now())}p(o,"failed");async function A(e,t){const a=await(await c()).prepare(`SELECT o.type, o.name, c.receiver_json, c.payload_json, m.object_id, i.external_id AS app,
|
|
2
2
|
d.message_id, owner.type AS owner_type, parent.external_id AS parent_app
|
|
3
3
|
FROM deliveries d JOIN objects o ON o.id = d.destination_id
|
|
4
4
|
JOIN custom_objects c ON c.object = o.id JOIN messages m ON m.message_id = d.message_id
|
|
5
5
|
LEFT JOIN objects owner ON owner.id = o.owner
|
|
6
6
|
LEFT JOIN identities i ON i.object = o.owner AND i.scheme = 'app'
|
|
7
7
|
LEFT JOIN identities parent ON parent.object = owner.owner AND parent.scheme = 'app'
|
|
8
|
-
WHERE d.delivery_id = ?`).get(e.deliveryId);if(!a||!a.receiver_json){await o(e,t,`delivery ${e.deliveryId} has no supported receiver`);return}const i=JSON.parse(a.receiver_json);if(!await O(e,t)){await d(e.deliveryId,e.attempts,t,{state:"acknowledged"},Date.now());return}if(typeof i=="object"&&typeof i.url=="string"&&a.type!=="action"){const n=await u(await c(),e.deliveryId);if(!n){await o(e,t,`delivery ${e.deliveryId} has no event document`);return}try{const r=await b(i.url,JSON.stringify(n),{"Content-Type":"application/json","X-Crtr-Delivery":e.deliveryId,"X-Crtr-Event":a.message_id},E);r>=200&&r<300?await d(e.deliveryId,e.attempts,t,{state:"acknowledged"},Date.now()):r>=300&&r<400?await o(e,t,`url receiver returned HTTP ${r}; redirects are not followed \u2014 register the final URL`):r>=400&&r<500&&r!==408&&r!==429?await o(e,t,`url receiver returned HTTP ${r}`):await d(e.deliveryId,e.attempts,t,{state:"pending",nextAttemptAt:Date.now()+f(e.attempts),failure:{kind:"spawn_error",message:`url receiver returned HTTP ${r}`}},Date.now())}catch(r){await d(e.deliveryId,e.attempts,t,{state:"pending",nextAttemptAt:Date.now()+f(e.attempts),failure:{kind:"spawn_error",message:String(r)}},Date.now())}return}if(typeof i!="object"||!Array.isArray(i.argv)||typeof i.cwd!="string"){await o(e,t,`delivery ${e.deliveryId} has an invalid argv receiver`);return}if(a.type==="action"){if(typeof i.name!="string"||!a.object_id){await o(e,t,`delivery ${e.deliveryId} has an invalid argv receiver`);return}const n=await N(e.deliveryId);if(n===null){await o(e,t,`delivery ${e.deliveryId} has no message body`);return}const{push:r,...y}=n,I={...y,action:{name:i.name,payload:a.payload_json===null?null:JSON.parse(a.payload_json)}};await w({argv:i.argv,cwd:i.cwd,document:I,attempt:e.attempts,nodeId:a.object_id,...a.app??a.parent_app?{app:a.app??a.parent_app}:{}},async _=>{await d(e.deliveryId,e.attempts,t,_,Date.now())});return}const s=await u(await c(),e.deliveryId);if(!s){await o(e,t,`delivery ${e.deliveryId} has no event document`);return}await w({argv:i.argv,cwd:i.cwd,document:s,attempt:e.attempts,app:a.app??a.parent_app??"app:terminal",custom:{deliveryId:e.deliveryId,messageId:a.message_id}},async n=>{await d(e.deliveryId,e.attempts,t,n,Date.now())})}p(
|
|
8
|
+
WHERE d.delivery_id = ?`).get(e.deliveryId);if(!a||!a.receiver_json){await o(e,t,`delivery ${e.deliveryId} has no supported receiver`);return}const i=JSON.parse(a.receiver_json);if(!await O(e,t)){await d(e.deliveryId,e.attempts,t,{state:"acknowledged"},Date.now());return}if(typeof i=="object"&&typeof i.url=="string"&&a.type!=="action"){const n=await u(await c(),e.deliveryId);if(!n){await o(e,t,`delivery ${e.deliveryId} has no event document`);return}try{const r=await b(i.url,JSON.stringify(n),{"Content-Type":"application/json","X-Crtr-Delivery":e.deliveryId,"X-Crtr-Event":a.message_id},E);r>=200&&r<300?await d(e.deliveryId,e.attempts,t,{state:"acknowledged"},Date.now()):r>=300&&r<400?await o(e,t,`url receiver returned HTTP ${r}; redirects are not followed \u2014 register the final URL`):r>=400&&r<500&&r!==408&&r!==429?await o(e,t,`url receiver returned HTTP ${r}`):await d(e.deliveryId,e.attempts,t,{state:"pending",nextAttemptAt:Date.now()+f(e.attempts),failure:{kind:"spawn_error",message:`url receiver returned HTTP ${r}`}},Date.now())}catch(r){await d(e.deliveryId,e.attempts,t,{state:"pending",nextAttemptAt:Date.now()+f(e.attempts),failure:{kind:"spawn_error",message:String(r)}},Date.now())}return}if(typeof i!="object"||!Array.isArray(i.argv)||typeof i.cwd!="string"){await o(e,t,`delivery ${e.deliveryId} has an invalid argv receiver`);return}if(a.type==="action"){if(typeof i.name!="string"||!a.object_id){await o(e,t,`delivery ${e.deliveryId} has an invalid argv receiver`);return}const n=await N(e.deliveryId);if(n===null){await o(e,t,`delivery ${e.deliveryId} has no message body`);return}const{push:r,...y}=n,I={...y,action:{name:i.name,payload:a.payload_json===null?null:JSON.parse(a.payload_json)}};await w({argv:i.argv,cwd:i.cwd,document:I,attempt:e.attempts,nodeId:a.object_id,...a.app??a.parent_app?{app:a.app??a.parent_app}:{}},async _=>{await d(e.deliveryId,e.attempts,t,_,Date.now())});return}const s=await u(await c(),e.deliveryId);if(!s){await o(e,t,`delivery ${e.deliveryId} has no event document`);return}await w({argv:i.argv,cwd:i.cwd,document:s,attempt:e.attempts,app:a.app??a.parent_app??"app:terminal",custom:{deliveryId:e.deliveryId,messageId:a.message_id}},async n=>{await d(e.deliveryId,e.attempts,t,n,Date.now())})}p(A,"deliverExternal");class H{static{p(this,"DeliveryLaneReconciler")}async run(t,a){if(!a.lifecycle.acceptsDetachedWork()||await T())return;let i;try{i=await j(t)}catch(s){m.fresh(()=>l({level:"error",event:"delivery.scan_failed",error:s}));return}for(const s of i)try{const n=await h(s.deliveryId,t,v);if(n===null)continue;a.lifecycle.registerDetached(A(n,v).catch(r=>{m.fresh(()=>l({level:"error",event:"delivery.failed",error:r,fields:{delivery_id:n.deliveryId}}))}))}catch(n){m.fresh(()=>l({level:"error",event:"delivery.claim_failed",error:n,fields:{delivery_id:s.deliveryId}}))}}}export{H as DeliveryLaneReconciler,v as deliveryDaemonInstanceId};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DetachedWorkLifecycle } from './broker-supervision.js';
|
|
2
|
+
export declare const IMPORT_LEASE_MS: number;
|
|
3
|
+
export declare class ImportLaneReconciler {
|
|
4
|
+
run(now: number, ctx: {
|
|
5
|
+
lifecycle: DetachedWorkLifecycle;
|
|
6
|
+
}): void;
|
|
7
|
+
/** One pass over the runs due now. Exposed for tests. */
|
|
8
|
+
pass(now: number): Promise<number>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var p=Object.defineProperty;var n=(i,e)=>p(i,"name",{value:e,configurable:!0});import{openDb as m}from"../../core/canvas/db.js";import{emitEvent as l}from"../../core/events/emit.js";import{operationIdContext as c}from"../../core/events/operation-id.js";import{processRun as f}from"../../core/import/load/run.js";import{ClaimLostError as d,withRowClaim as _}from"./row-claim.js";const R=2*6e4;class S{static{n(this,"ImportLaneReconciler")}run(e,o){if(!o.lifecycle.acceptsDetachedWork())return;const a=c.fresh(()=>this.pass(e).then(()=>{})).catch(t=>{c.fresh(()=>l({level:"error",event:"import.lane.failed",error:t}))});o.lifecycle.registerDetached(a)}async pass(e){const a=await(await m()).prepare("SELECT * FROM import_runs WHERE (state = 'loading' OR state = 'committed') AND (claimed_by IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= ?) ORDER BY created_at").all(new Date(e).toISOString());let t=0;for(const s of a)try{await _("import_runs",s.run_id,R,r=>f(s,r))&&(t+=1)}catch(r){if(!(r instanceof d))throw r}return t}}export{R as IMPORT_LEASE_MS,S as ImportLaneReconciler};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var N=Object.defineProperty;var s=(i,e)=>N(i,"name",{value:e,configurable:!0});import{clearFrozen as f,getNode as g,getRow as D}from"../../../core/canvas/index.js";import{openDb as c}from"../../../core/canvas/db.js";import{fullName as z}from"../../../core/canvas/labels.js";import{isSafeNodeId as d}from"../../../core/canvas/paths.js";import{emitEvent as _}from"../../../core/events/emit.js";import{operationIdContext as L}from"../../../core/events/operation-id.js";import{acknowledgedNodeMailCount as T}from"../../../core/canvas/deliveries.js";import{publishNotice as C}from"../../../core/runtime/close.js";import{transition as u}from"../../../core/runtime/lifecycle.js";import{isStartRefusal as y}from"../../../core/runtime/start-check.js";import{EnvironmentImagePending as A}from"./remote-wake.js";import{asNodePerson as M}from"./node-person.js";import{MachineLaunchUncertainError as b,MachineOfflineError as m}from"../../../core/runtime/runner/machine/ops.js";import{commitReopenResident as I}from"../../../core/runtime/reopen.js";import{reviveNode as w}from"../../../core/runtime/revive.js";import{hasPendingWake as k}from"../live-obligation.js";import{readInterruptedTurn as O}from"../../../core/canvas/recovery.js";import{mapAsync as P}from"../../../core/async-array.js";const h=6,U=10*6e4,x=2e4;async function H(i){const e=await(await c()).prepare("SELECT idle_release_attempts, idle_release_last_attempt_at, idle_release_acked_count FROM nodes WHERE node_id = ?").get(i);return{attempts:Number(e?.idle_release_attempts??0),acknowledgedCount:Number(e?.idle_release_acked_count??0),lastAttempt:e?.idle_release_last_attempt_at==null?void 0:Date.parse(e.idle_release_last_attempt_at)}}s(H,"readEpisode");async function F(i,e,a,t,r){const n=await(await c()).prepare("UPDATE nodes SET idle_release_attempts = ?, idle_release_last_attempt_at = ?, idle_release_acked_count = ? WHERE node_id = ? AND idle_release_attempts = ?").run(a,new Date(r).toISOString(),t,i,e);return Number(n.changes)===1}s(F,"claimAttempt");async function W(i,e){await(await c()).prepare("UPDATE nodes SET idle_release_attempts = ?, idle_release_last_attempt_at = ?, idle_release_acked_count = ? WHERE node_id = ?").run(e.attempts,e.lastAttempt===void 0?null:new Date(e.lastAttempt).toISOString(),e.acknowledgedCount,i)}s(W,"writeEpisode");function $(i,e){if(e)return 2;const a=i.launched_at;if(a==null)return 1;const t=i.last_settled_at;return t==null?2:Date.parse(t)>Date.parse(a)?1:2}s($,"thawTier");function B(i,e){const a=i.row.frozen_at,t=e.row.frozen_at;return a==null?t==null?0:1:t==null?-1:Date.parse(a)-Date.parse(t)}s(B,"compareThaw");class le{static{s(this,"FreezeLaneReconciler")}async endEpisode(e){await(await c()).prepare("UPDATE nodes SET idle_release_attempts = 0, idle_release_last_attempt_at = NULL, idle_release_acked_count = 0 WHERE node_id = ? AND (idle_release_attempts <> 0 OR idle_release_last_attempt_at IS NOT NULL OR idle_release_acked_count <> 0)").run(e)}async run(e,a){const t=await P(a.candidates,async n=>({...n,tier:$(n.row,await k(n.row.node_id))}));t.sort(B);const r={stalled:0,waitingOnMail:0};for(const n of t)await L.fresh(()=>M(n.row,()=>this.enact(e,n,a.fleet)))==="frozen"&&(n.tier===1?r.stalled+=1:r.waitingOnMail+=1);return r}async enact(e,a,t){const r=a.row.node_id;try{return a.enact==="thaw"?await this.enactThaw(a.row):a.enact==="reopen"?await this.enactReopen(a.row):a.enact==="idle-release"?await this.enactIdleRelease(e,a.row):(await t.applyDeadRowPolicy(r),await t.has(r)?"launched":(await D(r))?.frozen_at!=null?"frozen":"skipped")}catch(n){return _({level:"error",event:"node.lifecycle.enact_failed",...d(r)?{node_id:r}:{},error:n,fields:{...d(r)?{}:{affected_node_id:r},enact:a.enact}}),"skipped"}}async enactReopen(e){await I(e.node_id);try{return(await w(e.node_id,{resume:!0,capacity:"freeze"})).outcome==="frozen"?"frozen":"launched"}catch(a){if(y(a)||a instanceof A||a instanceof m)return await this.settleRefusedStart(e.node_id,a,"reopen");throw a}}async settleRefusedStart(e,a,t){return a instanceof b||await u(e,"release"),await f(e),_({level:"warn",event:"node.start.refused",node_id:e,error:a,fields:{enact:t}}),"skipped"}async enactThaw(e){const a=(await O(e.node_id))?.interrupted===!0;if(!a&&e.lifecycle==="resident"&&(await g(e.node_id))?.pi_session_id!=null&&!await k(e.node_id))return await u(e.node_id,"release"),await f(e.node_id),"skipped";try{return(await w(e.node_id,{resume:!0,capacity:"freeze",recovery:!0,...a?{wakeReason:"runtime-restart-abort"}:{}})).outcome==="frozen"?"frozen":"launched"}catch(t){if(y(t)||t instanceof A||t instanceof m)return await this.settleRefusedStart(e.node_id,t,"thaw");const r=await g(e.node_id);throw r!==null&&(r.status==="active"||r.status==="idle")&&(await u(e.node_id,"crash",{reason:"launch_failed",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",message:t instanceof Error?t.message:String(t)}}}),await f(e.node_id)),t}}async enactIdleRelease(e,a){const t=a.node_id,r=await T(t),n=await H(t),l=n.attempts===0||n.acknowledgedCount!==r,o=l?0:n.attempts;if(o>=h)return await this.endEpisode(t),_({level:"warn",event:"broker.idle_release.exhausted",...d(t)?{node_id:t}:{},fields:{...d(t)?{}:{affected_node_id:t},attempts:o,max_attempts:h}}),await u(t,"crash",{reason:"launch_failed",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",respawn_failures:o}}}),await C(t,"notice.crash",`Child crashed \u2014 ${z(a)} (${t}) failed to relaunch after ${o} attempts while trying to deliver a pending inbox message (no delivery progress across any attempt), and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${t}\`.`,{reason:"child-crashed",child:t}),"skipped";const E=l?void 0:n.lastAttempt,S=Math.min(x*2**o,U);if(E!==void 0&&e-E<S||!await F(t,n.attempts,o+1,r,e))return"skipped";const v=s(()=>W(t,{attempts:l?0:o,lastAttempt:l?void 0:n.lastAttempt,acknowledgedCount:r}),"giveBack");let R;try{R=await w(t,{resume:!0,capacity:"freeze"})}catch(p){if(!(p instanceof m))throw p;return await v(),await this.settleRefusedStart(t,p,"idle-release")}return R.outcome==="frozen"?(await v(),"frozen"):(_({level:"info",event:"broker.revive.idle_release",...d(t)?{node_id:t}:{},attempt:o+1,fields:{...d(t)?{}:{affected_node_id:t},attempts:o+1,max_attempts:h}}),"launched")}}export{le as FreezeLaneReconciler,B as compareThaw,$ as thawTier};
|