@north-light/cli 0.3.446 → 0.3.448

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/api/contract.d.ts +1 -1
  2. package/dist/api/contract.js +1 -1
  3. package/dist/api/dto/nodes.d.ts +5 -0
  4. package/dist/api/dto/objects.d.ts +33 -2
  5. package/dist/api/error-codes.d.ts +15 -0
  6. package/dist/api/error-codes.js +1 -1
  7. package/dist/clients/attach/viewer.js +578 -577
  8. package/dist/commands/canvas/list.js +3 -3
  9. package/dist/commands/canvas/read.js +4 -4
  10. package/dist/commands/node/inspect.js +8 -8
  11. package/dist/commands/sys/sync-shared.js +1 -1
  12. package/dist/commands/sys/update.js +1 -1
  13. package/dist/core/canvas/migrations.js +65 -31
  14. package/dist/core/feed/inbox.js +1 -1
  15. package/dist/core/graph/events.js +8 -8
  16. package/dist/core/graph/kinds/node.js +5 -5
  17. package/dist/core/graph/objects.js +8 -8
  18. package/dist/core/graph/reader.js +3 -3
  19. package/dist/core/graph/receivers.d.ts +2 -1
  20. package/dist/core/graph/receivers.js +5 -5
  21. package/dist/core/graph/spaces/app.js +1 -1
  22. package/dist/core/graph/types.d.ts +9 -1
  23. package/dist/core/peer/client.d.ts +2 -0
  24. package/dist/core/peer/client.js +1 -1
  25. package/dist/core/peer/directory.js +1 -1
  26. package/dist/core/peer/home.d.ts +40 -0
  27. package/dist/core/peer/home.js +2 -0
  28. package/dist/core/peer/ingest.d.ts +26 -0
  29. package/dist/core/peer/ingest.js +1 -0
  30. package/dist/core/peer/names.d.ts +3 -0
  31. package/dist/core/peer/names.js +1 -0
  32. package/dist/core/peer/outbox-store.d.ts +70 -0
  33. package/dist/core/peer/outbox-store.js +4 -0
  34. package/dist/core/peer/receiver.d.ts +8 -0
  35. package/dist/core/peer/receiver.js +1 -0
  36. package/dist/core/peer/stubs.d.ts +53 -0
  37. package/dist/core/peer/stubs.js +1 -0
  38. package/dist/core/peer/version.d.ts +2 -2
  39. package/dist/core/peer/version.js +1 -1
  40. package/dist/core/runtime/broker/inbox.js +1 -1
  41. package/dist/core/scoped-state/settings.js +1 -1
  42. package/dist/daemon/api/app-listener.js +9 -9
  43. package/dist/daemon/api/handlers/messages.js +2 -2
  44. package/dist/daemon/api/handlers/nodes.js +1 -1
  45. package/dist/daemon/api/handlers/objects.d.ts +4 -0
  46. package/dist/daemon/api/handlers/objects.js +2 -2
  47. package/dist/daemon/api/principal.d.ts +4 -4
  48. package/dist/daemon/api/principal.js +1 -1
  49. package/dist/daemon/api/sync-notice.d.ts +17 -0
  50. package/dist/daemon/api/sync-notice.js +1 -0
  51. package/dist/daemon/crtrd.js +8 -8
  52. package/dist/daemon/peer/forward.d.ts +69 -0
  53. package/dist/daemon/peer/forward.js +1 -0
  54. package/dist/daemon/peer/handlers.d.ts +6 -1
  55. package/dist/daemon/peer/handlers.js +1 -1
  56. package/dist/daemon/peer/local-ingress.d.ts +8 -0
  57. package/dist/daemon/peer/local-ingress.js +1 -0
  58. package/dist/daemon/peer/outbox.d.ts +38 -0
  59. package/dist/daemon/peer/outbox.js +1 -0
  60. package/dist/daemon/peer/relay-client.d.ts +35 -0
  61. package/dist/daemon/peer/relay-client.js +1 -0
  62. package/dist/daemon/peer/relay-protocol.d.ts +52 -0
  63. package/dist/daemon/peer/relay-protocol.js +1 -0
  64. package/dist/daemon/peer/relay-service.d.ts +26 -0
  65. package/dist/daemon/peer/relay-service.js +1 -0
  66. package/dist/shared/generated-context.d.ts +8 -0
  67. package/dist/shared/generated-context.js +4 -4
  68. package/dist/shared/inbox-entry-body.js +5 -5
  69. package/package.json +2 -2
  70. package/packages/crouter-identity/package.json +1 -1
  71. package/runtime.lock.json +12 -12
@@ -1,13 +1,13 @@
1
- var Se=Object.defineProperty;var s=(t,n)=>Se(t,"name",{value:n,configurable:!0});import{reportMachineStatus as _e}from"../core/peer/directory.js";import{writeFileSync as be,readFileSync as ke,rmSync as Ee,mkdirSync as Y,existsSync as k,chmodSync as Re}from"node:fs";import{createServer as X}from"node:http";import{dirname as Ie}from"node:path";import{envCrtrdTcp as xe}from"../shared/env.js";import{serveCanvasConversationStore as Pe}from"../core/canvas/sessions.js";import{pollRunObservers as Te}from"./runs/observer.js";import{randomBytes as De}from"node:crypto";import{apiSocketPath as Ce,crtrHome as Le}from"../core/canvas/paths.js";import{activeAppUsers as $e}from"../core/spaces/users.js";import{ensureAppSpace as Ae,ensureUserFiles as Ne}from"../core/spaces/space.js";import{LAUNCHER_SOCKET as R}from"../launcher/spec.js";import{launcherRequest as P}from"../launcher/client.js";import{installLauncherEventSink as Me}from"./launcher-events.js";import{initializeEmptyLayout as Ge,layoutNeedsMigration as Oe,socketDir as Z}from"../core/layout.js";import{migrateLooseKeys as Fe}from"../core/layout-migrate/loose-keys.js";import{getNode as He,listNodesForSupervision as Be,setFrozen as qe}from"../core/canvas/index.js";import{createApiServer as je}from"./api/server.js";import{createAppListener as ze}from"./api/app-listener.js";import{installLocalTerminalGrant as We}from"../core/grants/local.js";import{onRunEventAppended as Ue}from"../core/runs/events.js";import{runNotices as Ke}from"./run-notices.js";import{createGrantSync as Je,GRANT_SYNC_INTERVAL_S as Ve}from"../core/grants/sync.js";import{runRemovalCascades as Qe}from"../core/grants/remove.js";import{sweepSandboxes as Ye}from"../core/grants/on-sync.js";import{rebuildCronExecutions as Xe}from"./cron/run-socket.js";import{hasSyncedGrants as Ze}from"../core/grants/mirror.js";import{readConfig as er}from"../core/config.js";import{reconcileUndeliveredTickets as rr}from"./human/sweep.js";import{resetHandedOffDeliveries as tr}from"../core/canvas/deliveries.js";import{resetHandedOffDaemonActions as nr}from"../core/canvas/daemon-actions.js";import{sweepReviews as or}from"./review/sweep.js";import{pidfilePath as G}from"./pidfile.js";import{rendererWarning as ir}from"../core/runtime/package-health.js";import{DEFAULT_INTERVAL_MS as sr}from"./supervise-cadence.js";import{createResumeDetector as ar}from"./resume-detector.js";import{BrokerSupervisionReconciler as cr}from"./reconcilers/broker-supervision.js";import{ControllerDeathReconciler as dr}from"./reconcilers/controller-death.js";import{NodeLifecycleReconciler as lr}from"./reconcilers/node-lifecycle/tick.js";import{NodeDeadlineReconciler as pr}from"./reconcilers/node-deadline.js";import{CronLaneReconciler as mr}from"./reconcilers/cron-lane.js";import{DeliveryLaneReconciler as fr}from"./reconcilers/delivery-lane.js";import{ActionLaneReconciler as ur}from"./reconcilers/action-lane.js";import{endLiveExecution as hr,migrateLegacyExecutionIdentities as vr}from"../core/canvas/executions.js";import{revokePriorEpochNodeTokens as wr}from"../core/canvas/node-tokens.js";import{PendingReviewSubmitReconciler as gr}from"./reconcilers/pending-review-submit.js";import{StorageMaintenanceReconciler as yr}from"./reconcilers/storage-maintenance.js";import{StorageDueWorkReconciler as Sr}from"./reconcilers/storage-due-work.js";import{BashDeadlineReconciler as _r}from"./reconcilers/bash-deadline.js";import{FaultRetryBackstopReconciler as br}from"./reconcilers/fault-retry-backstop.js";import{StructuredRequestReconciler as kr}from"./reconcilers/structured-request-lane.js";import{reconcileStructuredRequestSidecars as Er}from"./structured-requests/boot.js";import{captureLivenessSnapshot as ee,capturePidCommand as Rr,captureTeardownSnapshot as re,isPidAlive as Ir,isAnyPidAlive as te,killProcessTreePids as ne}from"../core/canvas/pid.js";import{bindFleet as xr,boundFleet as Pr}from"../core/runtime/fleet.js";import{bindDaemonControl as Tr,clearDaemonControl as Dr}from"./control.js";import{execDaemon as Cr,spawnDaemon as Lr}from"./manage.js";import{reconcileAndMigrateBoot as $r}from"../core/canvas/boot.js";import{resolveInstallId as Ar}from"../core/canvas/install-id.js";import{setInstallId as Nr}from"../core/runtime/nodes.js";import{claimDaemonOwnership as Mr,releaseDaemonOwnership as oe}from"../core/canvas/daemon-owner.js";import{migrate as Gr,openDb as ie,withCanvasWrite as Or,bindDaemonCanvasOpenHook as Fr,clearDaemonCanvasOpenHook as se}from"../core/canvas/db.js";import{brokerSignalBus as O}from"./broker-signals/bus.js";import{brokerSignalStreams as Hr}from"./api/handlers/broker-signals.js";import{reconcilePackageDocuments as Br}from"../core/graph/package-docs.js";import{startRepoSync as qr}from"../core/graph/repo-sync/index.js";import{prewarmRecentRecipes as jr,reapStaleSpares as zr}from"../core/runtime/warm-pool.js";import{DaemonFleet as Wr}from"./fleet.js";import{BOOT_MIGRATIONS as Ur}from"./boot-migrations.js";import{applyJoinedOwner as Kr}from"./joined-owner.js";import{emitEvent as u}from"../core/events/emit.js";import{operationIdContext as _}from"../core/events/operation-id.js";import{bindDaemonEventSource as Jr}from"../core/events/source.js";import{activateCanvasImport as Vr,MigrationBlockedError as Qr,runForcedCanvasImport as Yr}from"../migrations/activation.js";import{convertLegacyProviderStore as Xr,convertLegacyScopedStore as Zr}from"../core/scoped-state/migrate.js";import{userScopeRoot as et}from"../core/scope.js";import{secretsDir as rt}from"../core/layout.js";function F(t,n){return t.includes(` --canvas-home ${n} `)}s(F,"brokerCommandDeclaresCanvasHome");function H(t){return/ --epoch (\S+) /.exec(t)?.[1]??null}s(H,"brokerCommandEpoch");const tt=5e3,ae=150;async function nt(t){const n=ee();if(n===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness"}}),new Error("prior-epoch broker census could not capture the process table");const a=Le(),m=[];for(const[i,p]of n.processes){const y=p.command;if(p.zombie||!y.includes("broker-cli.js")||!F(y,a)||H(y)===t)continue;const S=y.trim().split(/\s+/).at(-1)??"";S===""||S.endsWith("broker-cli.js")||m.push({pid:i,nodeId:S})}let f=0;const o=[];for(const i of m){const p=Rr(i.pid);if(p===null||!p.includes("broker-cli.js")||!F(p,a)||H(p)===t||p.trim().split(/\s+/).at(-1)!==i.nodeId)continue;const y=re(i.pid,null);if(!y.reused){try{process.platform==="linux"&&k(R)?await P({operation:"stop",pid:i.pid}):process.kill(i.pid,"SIGTERM")}catch{continue}f++,o.push({pid:i.pid,snap:y})}}const w=Date.now()+tt;for(;Date.now()<w&&o.some(i=>te(i.snap.tree.length>0?i.snap.tree:[i.pid]));)await new Promise(i=>setTimeout(i,ae));let v=0;for(const i of o){const p=i.snap.tree.length>0?i.snap.tree:[i.pid];te(p)&&(process.platform==="linux"&&k(R)?await P({operation:"stop",pid:i.pid}):ne(p,"SIGKILL",i.snap.identities),v++)}v>0&&await new Promise(i=>setTimeout(i,ae));const d=ee();if(d===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness",stage:"final"}}),new Error("prior-epoch broker census could not capture the final process table");const l=[];for(const[i,p]of d.processes){const y=p.command;p.zombie||!y.includes("broker-cli.js")||F(y,a)&&H(y)!==t&&l.push(i)}if(l.length>0)throw new Error(`prior-epoch broker census could not prove termination of pid(s): ${l.join(", ")}`);u({level:"info",event:"daemon.census.completed",fields:{prior_epoch_brokers:m.length,sigtermed:f,sigkilled:v}})}s(nt,"reapPriorEpochBrokers");const ot=5e3;async function it(t){const n=Array.from(t.entries());if(n.length===0)return;for(const[o]of n){const w=He(o)?.status;(w==="active"||w==="idle")&&qe(o,new Date().toISOString())}const a=n.map(([,o])=>({pid:o.pid,snap:re(o.pid,null),exited:o.handle.exited}));for(const o of a)if(!o.snap.reused)try{process.platform==="linux"&&k(R)?await P({operation:"stop",pid:o.pid}):process.kill(o.pid,"SIGTERM")}catch{}const m=new Promise(o=>{setTimeout(o,ot).unref()});await Promise.allSettled(a.map(o=>Promise.race([o.exited.then(()=>{}),m])));const f=[];for(const o of a)Ir(o.pid)&&(process.platform==="linux"&&k(R)?await P({operation:"stop",pid:o.pid}):ne(o.snap.tree.length>0?o.snap.tree:[o.pid],"SIGKILL",o.snap.identities),f.push(o));await Promise.all(f.map(o=>o.exited));for(const[o]of n)hr(o,{reason:"daemon-teardown"});u({level:"info",event:"daemon.fleet_teardown.completed",fields:{brokers:a.length,sigkilled:f.length}})}s(it,"teardownFleet");function ce(){Y(Ie(G()),{recursive:!0}),be(G(),String(process.pid),"utf8")}s(ce,"writePidfile");function de(t){return typeof t=="object"&&t!==null&&t.code==="ENOENT"}s(de,"isMissingPathError");function le(t){const n=G();let a;try{a=ke(n,"utf8")}catch(m){if(de(m))return;throw m}if(a.trim()!==String(t))throw new Error(`daemon readiness mirror does not name this invocation (expected pid ${t})`);try{Ee(n)}catch(m){if(de(m))return;throw m}}s(le,"removeOwnedPidfileMirror");const st={acceptsDetachedWork:s(()=>!0,"acceptsDetachedWork"),registerDetached:s(t=>{t.catch(()=>{})},"registerDetached")},B=new cr,at=new dr,ct=new lr,dt=new pr,lt=new mr,pt=new fr,mt=new ur,ft=new gr,ut=new yr,ht=new Sr,vt=new _r,wt=new br,gt=new kr;async function yt(t=Date.now(),n=st){const a=Pr(),m=await B.runFleetPreamble(a);let f,o;try{const v=Be();f=v.filter(d=>d.status==="active"||d.status==="idle"),o=v.filter(d=>d.status==="done"||d.status==="canceled"||d.status==="dead")}catch(v){u({level:"error",event:"canvas.node_list.failed",error:v});return}await B.run(t,{rows:f,fleet:a,lifecycle:n}),at.run(t,{rows:f});const w=await ct.run(t,{rows:[...f,...o],fleet:a});dt.run(t),gt.run(t),lt.run(t,{lifecycle:n}),pt.run(t,{lifecycle:n}),mt.run(t,{lifecycle:n}),ft.run(t,{lifecycle:n}),ut.run(t,{lifecycle:n,fleet:a}),ht.run(t,{lifecycle:n}),Te(),wt.run(t,{fleet:a,lifecycle:n}),await vt.run(t,{rows:f}),B.warnIfSaturated({fleet:a,lifecycle:n,thresholds:m,frozen:w})}s(yt,"superviseTick");async function St(t){Y(Z(),{recursive:!0,mode:493}),process.platform==="linux"&&Re(Z(),493);const n=Ce();if(k(n))throw new Error(`layout refusal cannot bind occupied socket: ${n}`);const a=X((d,l)=>{if(d.method==="GET"&&d.url==="/healthz"){l.writeHead(200,{"content-type":"application/json; charset=utf-8"}),l.end(JSON.stringify({ok:!1,daemon_up:!0,store_accessible:!1,brokers_reconciled:!1,startup_phase:"blocked",startup_blocked:"layout_migration_required"}));return}l.writeHead(503,{"content-type":"application/json; charset=utf-8"}),l.end(JSON.stringify({error:{code:"layout_migration_required",message:"run crtr sys migrate while the runtime is offline",type:"server_error",origin:"runtime",retryable:!1}}))});await new Promise((d,l)=>{a.once("error",l),a.listen(n,()=>{a.off("error",l),d()})});let m;const f=t.tcp??xe();if(f){const d=/^(.*):(\d+)$/.exec(f);if(!d||Number(d[2])>65535)throw new Error(`invalid refusal TCP address: ${f}`);const l=X(a.listeners("request")[0]);await new Promise((i,p)=>{l.once("error",p),l.listen(Number(d[2]),d[1],()=>{l.off("error",p),i()})}),m=l}ce();let o;const w=s(()=>(o??=(async()=>{a.closeAllConnections(),m?.closeAllConnections(),await Promise.all([a,m].filter(d=>d!==void 0).map(d=>new Promise((l,i)=>d.close(p=>p?i(p):l())))),le(process.pid),process.off("SIGINT",v),process.off("SIGTERM",v)})(),o),"cleanup"),v=s(()=>{w().then(()=>process.exit(0),d=>{process.stderr.write(`[crtrd] layout refusal cleanup failed: ${String(d)}
2
- `),process.exit(1)})},"onSignal");return process.on("SIGINT",v),process.on("SIGTERM",v),w}s(St,"serveLayoutRefusal");async function _t(t={}){if(Oe())return St(t);Ge();const n=Mr(De(16).toString("hex"));if(!n.claimed){if(n.reason==="incumbent_alive")process.stderr.write(`[crtrd] already running (pid ${n.incumbentPid??"?"})
1
+ var _e=Object.defineProperty;var s=(t,n)=>_e(t,"name",{value:n,configurable:!0});import{startPeerOutbox as ke}from"./peer/outbox.js";import{reportMachineStatus as Ee}from"../core/peer/directory.js";import{createPeerRelay as Re}from"./peer/relay-service.js";import{writeFileSync as xe,readFileSync as Pe,rmSync as Ie,mkdirSync as Z,existsSync as k,chmodSync as Te}from"node:fs";import{createServer as ee}from"node:http";import{dirname as De}from"node:path";import{envCrtrdTcp as Ce}from"../shared/env.js";import{serveCanvasConversationStore as Le}from"../core/canvas/sessions.js";import{pollRunObservers as $e}from"./runs/observer.js";import{randomBytes as Ae}from"node:crypto";import{apiSocketPath as Ne,crtrHome as Me}from"../core/canvas/paths.js";import{activeAppUsers as Oe}from"../core/spaces/users.js";import{ensureAppSpace as Ge,ensureUserFiles as Fe}from"../core/spaces/space.js";import{LAUNCHER_SOCKET as x}from"../launcher/spec.js";import{launcherRequest as I}from"../launcher/client.js";import{installLauncherEventSink as He}from"./launcher-events.js";import{initializeEmptyLayout as Be,layoutNeedsMigration as je,socketDir as re}from"../core/layout.js";import{migrateLooseKeys as qe}from"../core/layout-migrate/loose-keys.js";import{getNode as ze,listNodesForSupervision as We,setFrozen as Ue}from"../core/canvas/index.js";import{createApiServer as Ke}from"./api/server.js";import{createAppListener as Je}from"./api/app-listener.js";import{installLocalTerminalGrant as Ve}from"../core/grants/local.js";import{onRunEventAppended as Qe}from"../core/runs/events.js";import{runNotices as Ye}from"./run-notices.js";import{createGrantSync as Xe,GRANT_SYNC_INTERVAL_S as Ze}from"../core/grants/sync.js";import{runRemovalCascades as er}from"../core/grants/remove.js";import{sweepSandboxes as rr}from"../core/grants/on-sync.js";import{rebuildCronExecutions as tr}from"./cron/run-socket.js";import{hasSyncedGrants as nr}from"../core/grants/mirror.js";import{readConfig as or}from"../core/config.js";import{reconcileUndeliveredTickets as ir}from"./human/sweep.js";import{resetHandedOffDeliveries as sr}from"../core/canvas/deliveries.js";import{resetHandedOffDaemonActions as ar}from"../core/canvas/daemon-actions.js";import{sweepReviews as cr}from"./review/sweep.js";import{pidfilePath as F}from"./pidfile.js";import{rendererWarning as dr}from"../core/runtime/package-health.js";import{DEFAULT_INTERVAL_MS as lr}from"./supervise-cadence.js";import{createResumeDetector as pr}from"./resume-detector.js";import{BrokerSupervisionReconciler as mr}from"./reconcilers/broker-supervision.js";import{ControllerDeathReconciler as fr}from"./reconcilers/controller-death.js";import{NodeLifecycleReconciler as ur}from"./reconcilers/node-lifecycle/tick.js";import{NodeDeadlineReconciler as hr}from"./reconcilers/node-deadline.js";import{CronLaneReconciler as vr}from"./reconcilers/cron-lane.js";import{DeliveryLaneReconciler as wr}from"./reconcilers/delivery-lane.js";import{ActionLaneReconciler as gr}from"./reconcilers/action-lane.js";import{endLiveExecution as yr,migrateLegacyExecutionIdentities as Sr}from"../core/canvas/executions.js";import{revokePriorEpochNodeTokens as br}from"../core/canvas/node-tokens.js";import{PendingReviewSubmitReconciler as _r}from"./reconcilers/pending-review-submit.js";import{StorageMaintenanceReconciler as kr}from"./reconcilers/storage-maintenance.js";import{StorageDueWorkReconciler as Er}from"./reconcilers/storage-due-work.js";import{BashDeadlineReconciler as Rr}from"./reconcilers/bash-deadline.js";import{FaultRetryBackstopReconciler as xr}from"./reconcilers/fault-retry-backstop.js";import{StructuredRequestReconciler as Pr}from"./reconcilers/structured-request-lane.js";import{reconcileStructuredRequestSidecars as Ir}from"./structured-requests/boot.js";import{captureLivenessSnapshot as te,capturePidCommand as Tr,captureTeardownSnapshot as ne,isPidAlive as Dr,isAnyPidAlive as oe,killProcessTreePids as ie}from"../core/canvas/pid.js";import{bindFleet as Cr,boundFleet as Lr}from"../core/runtime/fleet.js";import{bindDaemonControl as $r,clearDaemonControl as Ar}from"./control.js";import{execDaemon as Nr,spawnDaemon as Mr}from"./manage.js";import{reconcileAndMigrateBoot as Or}from"../core/canvas/boot.js";import{resolveInstallId as Gr}from"../core/canvas/install-id.js";import{setInstallId as Fr}from"../core/runtime/nodes.js";import{claimDaemonOwnership as Hr,releaseDaemonOwnership as se}from"../core/canvas/daemon-owner.js";import{migrate as Br,openDb as ae,withCanvasWrite as jr,bindDaemonCanvasOpenHook as qr,clearDaemonCanvasOpenHook as ce}from"../core/canvas/db.js";import{brokerSignalBus as H}from"./broker-signals/bus.js";import{brokerSignalStreams as zr}from"./api/handlers/broker-signals.js";import{reconcilePackageDocuments as Wr}from"../core/graph/package-docs.js";import{startRepoSync as Ur}from"../core/graph/repo-sync/index.js";import{prewarmRecentRecipes as Kr,reapStaleSpares as Jr}from"../core/runtime/warm-pool.js";import{DaemonFleet as Vr}from"./fleet.js";import{BOOT_MIGRATIONS as Qr}from"./boot-migrations.js";import{applyJoinedOwner as Yr}from"./joined-owner.js";import{emitEvent as u}from"../core/events/emit.js";import{operationIdContext as b}from"../core/events/operation-id.js";import{bindDaemonEventSource as Xr}from"../core/events/source.js";import{activateCanvasImport as Zr,MigrationBlockedError as et,runForcedCanvasImport as rt}from"../migrations/activation.js";import{convertLegacyProviderStore as tt,convertLegacyScopedStore as nt}from"../core/scoped-state/migrate.js";import{userScopeRoot as ot}from"../core/scope.js";import{secretsDir as it}from"../core/layout.js";function B(t,n){return t.includes(` --canvas-home ${n} `)}s(B,"brokerCommandDeclaresCanvasHome");function j(t){return/ --epoch (\S+) /.exec(t)?.[1]??null}s(j,"brokerCommandEpoch");const st=5e3,de=150;async function at(t){const n=te();if(n===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness"}}),new Error("prior-epoch broker census could not capture the process table");const a=Me(),m=[];for(const[i,p]of n.processes){const y=p.command;if(p.zombie||!y.includes("broker-cli.js")||!B(y,a)||j(y)===t)continue;const S=y.trim().split(/\s+/).at(-1)??"";S===""||S.endsWith("broker-cli.js")||m.push({pid:i,nodeId:S})}let f=0;const o=[];for(const i of m){const p=Tr(i.pid);if(p===null||!p.includes("broker-cli.js")||!B(p,a)||j(p)===t||p.trim().split(/\s+/).at(-1)!==i.nodeId)continue;const y=ne(i.pid,null);if(!y.reused){try{process.platform==="linux"&&k(x)?await I({operation:"stop",pid:i.pid}):process.kill(i.pid,"SIGTERM")}catch{continue}f++,o.push({pid:i.pid,snap:y})}}const g=Date.now()+st;for(;Date.now()<g&&o.some(i=>oe(i.snap.tree.length>0?i.snap.tree:[i.pid]));)await new Promise(i=>setTimeout(i,de));let v=0;for(const i of o){const p=i.snap.tree.length>0?i.snap.tree:[i.pid];oe(p)&&(process.platform==="linux"&&k(x)?await I({operation:"stop",pid:i.pid}):ie(p,"SIGKILL",i.snap.identities),v++)}v>0&&await new Promise(i=>setTimeout(i,de));const d=te();if(d===null)throw u({level:"error",event:"daemon.census.snapshot_failed",fields:{invariant:"prior-epoch brokers are reaped before readiness",stage:"final"}}),new Error("prior-epoch broker census could not capture the final process table");const l=[];for(const[i,p]of d.processes){const y=p.command;p.zombie||!y.includes("broker-cli.js")||B(y,a)&&j(y)!==t&&l.push(i)}if(l.length>0)throw new Error(`prior-epoch broker census could not prove termination of pid(s): ${l.join(", ")}`);u({level:"info",event:"daemon.census.completed",fields:{prior_epoch_brokers:m.length,sigtermed:f,sigkilled:v}})}s(at,"reapPriorEpochBrokers");const ct=5e3;async function dt(t){const n=Array.from(t.entries());if(n.length===0)return;for(const[o]of n){const g=ze(o)?.status;(g==="active"||g==="idle")&&Ue(o,new Date().toISOString())}const a=n.map(([,o])=>({pid:o.pid,snap:ne(o.pid,null),exited:o.handle.exited}));for(const o of a)if(!o.snap.reused)try{process.platform==="linux"&&k(x)?await I({operation:"stop",pid:o.pid}):process.kill(o.pid,"SIGTERM")}catch{}const m=new Promise(o=>{setTimeout(o,ct).unref()});await Promise.allSettled(a.map(o=>Promise.race([o.exited.then(()=>{}),m])));const f=[];for(const o of a)Dr(o.pid)&&(process.platform==="linux"&&k(x)?await I({operation:"stop",pid:o.pid}):ie(o.snap.tree.length>0?o.snap.tree:[o.pid],"SIGKILL",o.snap.identities),f.push(o));await Promise.all(f.map(o=>o.exited));for(const[o]of n)yr(o,{reason:"daemon-teardown"});u({level:"info",event:"daemon.fleet_teardown.completed",fields:{brokers:a.length,sigkilled:f.length}})}s(dt,"teardownFleet");function le(){Z(De(F()),{recursive:!0}),xe(F(),String(process.pid),"utf8")}s(le,"writePidfile");function pe(t){return typeof t=="object"&&t!==null&&t.code==="ENOENT"}s(pe,"isMissingPathError");function me(t){const n=F();let a;try{a=Pe(n,"utf8")}catch(m){if(pe(m))return;throw m}if(a.trim()!==String(t))throw new Error(`daemon readiness mirror does not name this invocation (expected pid ${t})`);try{Ie(n)}catch(m){if(pe(m))return;throw m}}s(me,"removeOwnedPidfileMirror");const lt={acceptsDetachedWork:s(()=>!0,"acceptsDetachedWork"),registerDetached:s(t=>{t.catch(()=>{})},"registerDetached")},q=new mr,pt=new fr,mt=new ur,ft=new hr,ut=new vr,ht=new wr,vt=new gr,wt=new _r,gt=new kr,yt=new Er,St=new Rr,bt=new xr,_t=new Pr;async function kt(t=Date.now(),n=lt){const a=Lr(),m=await q.runFleetPreamble(a);let f,o;try{const v=We();f=v.filter(d=>d.status==="active"||d.status==="idle"),o=v.filter(d=>d.status==="done"||d.status==="canceled"||d.status==="dead")}catch(v){u({level:"error",event:"canvas.node_list.failed",error:v});return}await q.run(t,{rows:f,fleet:a,lifecycle:n}),pt.run(t,{rows:f});const g=await mt.run(t,{rows:[...f,...o],fleet:a});ft.run(t),_t.run(t),ut.run(t,{lifecycle:n}),ht.run(t,{lifecycle:n}),vt.run(t,{lifecycle:n}),wt.run(t,{lifecycle:n}),gt.run(t,{lifecycle:n,fleet:a}),yt.run(t,{lifecycle:n}),$e(),bt.run(t,{fleet:a,lifecycle:n}),await St.run(t,{rows:f}),q.warnIfSaturated({fleet:a,lifecycle:n,thresholds:m,frozen:g})}s(kt,"superviseTick");async function Et(t){Z(re(),{recursive:!0,mode:493}),process.platform==="linux"&&Te(re(),493);const n=Ne();if(k(n))throw new Error(`layout refusal cannot bind occupied socket: ${n}`);const a=ee((d,l)=>{if(d.method==="GET"&&d.url==="/healthz"){l.writeHead(200,{"content-type":"application/json; charset=utf-8"}),l.end(JSON.stringify({ok:!1,daemon_up:!0,store_accessible:!1,brokers_reconciled:!1,startup_phase:"blocked",startup_blocked:"layout_migration_required"}));return}l.writeHead(503,{"content-type":"application/json; charset=utf-8"}),l.end(JSON.stringify({error:{code:"layout_migration_required",message:"run crtr sys migrate while the runtime is offline",type:"server_error",origin:"runtime",retryable:!1}}))});await new Promise((d,l)=>{a.once("error",l),a.listen(n,()=>{a.off("error",l),d()})});let m;const f=t.tcp??Ce();if(f){const d=/^(.*):(\d+)$/.exec(f);if(!d||Number(d[2])>65535)throw new Error(`invalid refusal TCP address: ${f}`);const l=ee(a.listeners("request")[0]);await new Promise((i,p)=>{l.once("error",p),l.listen(Number(d[2]),d[1],()=>{l.off("error",p),i()})}),m=l}le();let o;const g=s(()=>(o??=(async()=>{a.closeAllConnections(),m?.closeAllConnections(),await Promise.all([a,m].filter(d=>d!==void 0).map(d=>new Promise((l,i)=>d.close(p=>p?i(p):l())))),me(process.pid),process.off("SIGINT",v),process.off("SIGTERM",v)})(),o),"cleanup"),v=s(()=>{g().then(()=>process.exit(0),d=>{process.stderr.write(`[crtrd] layout refusal cleanup failed: ${String(d)}
2
+ `),process.exit(1)})},"onSignal");return process.on("SIGINT",v),process.on("SIGTERM",v),g}s(Et,"serveLayoutRefusal");async function Rt(t={}){if(je())return Et(t);Be();const n=Hr(Ae(16).toString("hex"));if(!n.claimed){if(n.reason==="incumbent_alive")process.stderr.write(`[crtrd] already running (pid ${n.incumbentPid??"?"})
3
3
  `);else{const r=[n.incumbentPid!==void 0?`pid ${n.incumbentPid}`:void 0,n.verdict!==void 0?`verdict ${n.verdict}`:void 0].filter(c=>c!==void 0),e=r.length>0?` (${r.join(", ")})`:"";process.stderr.write(`[crtrd] claim lost: ${n.reason}${e}
4
- `),n.reason==="own_identity_indeterminate"&&process.stderr.write("[crtrd] cannot read its own process identity. crouter needs /proc on Linux, or the `ps` command elsewhere (Debian/Ubuntu: apt-get install procps).\n"),(n.reason==="own_identity_indeterminate"||n.reason==="incumbent_malformed")&&(process.exitCode=1)}return async()=>{}}const a=n.claim;let m=null;try{if(await nt(a.token),Zr({kind:"user",root:et()}),process.platform==="darwin"){const{readConfig:e,updateConfig:c}=await import("../core/config.js");if(e("user").paths?.user_files===void 0){const{homedir:g}=await import("node:os");c("user",Q=>{Q.paths??={},Q.paths.user_files??=g()})}}await Xr(rt());for(const e of Fe())process.stderr.write(`[crtrd] ${e}
5
- `);Gr(ie()),Fr(e=>O.install(e));const r=Or(e=>Br(e));for(const e of r.conflicts)process.stderr.write(`[crtrd] plugin documents not reconciled: ${e.message}
4
+ `),n.reason==="own_identity_indeterminate"&&process.stderr.write("[crtrd] cannot read its own process identity. crouter needs /proc on Linux, or the `ps` command elsewhere (Debian/Ubuntu: apt-get install procps).\n"),(n.reason==="own_identity_indeterminate"||n.reason==="incumbent_malformed")&&(process.exitCode=1)}return async()=>{}}const a=n.claim;let m=null;try{if(await at(a.token),nt({kind:"user",root:ot()}),process.platform==="darwin"){const{readConfig:e,updateConfig:c}=await import("../core/config.js");if(e("user").paths?.user_files===void 0){const{homedir:w}=await import("node:os");c("user",X=>{X.paths??={},X.paths.user_files??=w()})}}await tt(it());for(const e of qe())process.stderr.write(`[crtrd] ${e}
5
+ `);Br(ae()),qr(e=>H.install(e));const r=jr(e=>Wr(e));for(const e of r.conflicts)process.stderr.write(`[crtrd] plugin documents not reconciled: ${e.message}
6
6
  `);for(const e of r.invalid)process.stderr.write(`[crtrd] package file is not a document: ${e.path}: ${e.error}
7
7
  `);for(const e of r.quarantined)process.stderr.write(`[crtrd] plugin "${e.name}" has a damaged ${e.file} (${e.reason}); ${e.movedTo!==void 0?`moved to ${e.movedTo}`:"left in place and skipped"}
8
- `);m=Kr(),process.platform==="linux"&&k(R)&&(Me(),await P({operation:"users.sync",users:$e()})),wr(a.token)}catch(r){se(),O.close();try{const{released:e}=oe(a);if(!e)throw new Error("daemon ownership claim was not released")}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}let f=!0,o,w=Promise.resolve(),v=!1,d,l;const i=new Set;let p=!1,y=!1,S,L,q=!1,$,T,D;const pe=ar();let I,h={phase:"initializing",message:"daemon startup is running the canvas import and normal services are unavailable",blockerPaths:[]},x="initial startup activation",me=null;function fe(){return h.phase==="ready"?null:{phase:h.phase,message:h.message,blockerPaths:h.blockerPaths}}s(fe,"startupBlock");function b(r){i.add(r),r.finally(()=>{i.delete(r)}).catch(()=>{})}s(b,"registerDetached");const j={acceptsDetachedWork:s(()=>f,"acceptsDetachedWork"),registerDetached:b};qr(j,ie);async function ue(){try{f=!1,Hr.close(),se(),O.close(),d!==void 0&&(clearInterval(d),d=void 0),D!==void 0&&(clearInterval(D),D=void 0);let r;do r=w,await r;while(r!==w);for(;i.size>0;)await Promise.allSettled([...i]);if(l!==void 0&&(await it(l),l.dispose()),L!==void 0&&!q&&(q=!0,await L.close()),T!==void 0){const e=T;T=void 0,await e.close()}if($!==void 0){const e=$;$=void 0,await e.close()}if(p||(le(process.pid),p=!0),p&&!y){const{released:e}=oe(a);if(!e)throw new Error("daemon ownership claim was not released");y=!0}}finally{Dr(),process.off("SIGINT",U),process.off("SIGTERM",K),process.off("exit",J)}}s(ue,"cleanupAttempt");const A=1e3;let z=!1;function he(){return z?{graceMs:A}:(z=!0,_.fresh(()=>{u({level:"info",event:"daemon.handover.armed",fields:{pid:process.pid,grace_ms:A}})}),setTimeout(()=>{ve()},A),{graceMs:A})}s(he,"requestHandover");async function ve(){try{await E()}catch(r){const e=r instanceof Error?r.stack??r.message:String(r);process.stderr.write(`[crtrd] handover cleanup failed:
8
+ `);m=Yr(),process.platform==="linux"&&k(x)&&(He(),await I({operation:"users.sync",users:Oe()})),br(a.token)}catch(r){ce(),H.close();try{const{released:e}=se(a);if(!e)throw new Error("daemon ownership claim was not released")}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}let f=!0,o,g=Promise.resolve(),v=!1,d,l;const i=new Set;let p=!1,y=!1,S,$,z=!1,A,T,D,C,M;const fe=pr();let E,h={phase:"initializing",message:"daemon startup is running the canvas import and normal services are unavailable",blockerPaths:[]},P="initial startup activation",ue=null;function he(){return h.phase==="ready"?null:{phase:h.phase,message:h.message,blockerPaths:h.blockerPaths}}s(he,"startupBlock");function _(r){i.add(r),r.finally(()=>{i.delete(r)}).catch(()=>{})}s(_,"registerDetached");const W={acceptsDetachedWork:s(()=>f,"acceptsDetachedWork"),registerDetached:_};Ur(W,ae);async function ve(){try{f=!1,zr.close(),ce(),H.close(),d!==void 0&&(clearInterval(d),d=void 0),D!==void 0&&(clearInterval(D),D=void 0),M?.(),M=void 0;let r;do r=g,await r;while(r!==g);for(;i.size>0;)await Promise.allSettled([...i]);if(l!==void 0&&(await dt(l),l.dispose()),$!==void 0&&!z&&(z=!0,await $.close()),C!==void 0){const e=C;C=void 0,await e.stop()}if(T!==void 0){const e=T;T=void 0,await e.close()}if(A!==void 0){const e=A;A=void 0,await e.close()}if(p||(me(process.pid),p=!0),p&&!y){const{released:e}=se(a);if(!e)throw new Error("daemon ownership claim was not released");y=!0}}finally{Ar(),process.off("SIGINT",J),process.off("SIGTERM",V),process.off("exit",Q)}}s(ve,"cleanupAttempt");const N=1e3;let U=!1;function we(){return U?{graceMs:N}:(U=!0,b.fresh(()=>{u({level:"info",event:"daemon.handover.armed",fields:{pid:process.pid,grace_ms:N}})}),setTimeout(()=>{ge()},N),{graceMs:N})}s(we,"requestHandover");async function ge(){try{await R()}catch(r){const e=r instanceof Error?r.stack??r.message:String(r);process.stderr.write(`[crtrd] handover cleanup failed:
9
9
  ${e}
10
- `),process.exit(1)}try{process.platform==="linux"&&k(R)&&(await P({operation:"daemon.handover"}),process.exit(0)),process.pid===1&&await Cr();const r=await Lr({waitForJoinLock:!0});_.fresh(()=>{u({level:"info",event:"daemon.handover.completed",fields:{predecessor_pid:process.pid,successor_pid:r.pid??r.existing_pid??null,spawned:r.started}})})}catch(r){_.fresh(()=>{u({level:"error",event:"daemon.handover.successor_failed",error:r})});const e=r instanceof Error?r.message:String(r);process.stderr.write(`[crtrd] handover successor failed to start: ${e}
11
- `),process.exit(1)}process.exit(0)}s(ve,"performHandover");function E(){if(S!==void 0)return S;const r=ue();return S=r,r.then(()=>{S===r&&(S=void 0)},()=>{S===r&&(S=void 0)}),r}s(E,"requestCleanup");async function W(r){try{await E(),process.exit(0)}catch(e){process.exitCode=1;const c=e instanceof Error?e.stack??`${e.name}: ${e.message}`:String(e);process.stderr.write(`[crtrd] ${r} cleanup failed:
10
+ `),process.exit(1)}try{process.platform==="linux"&&k(x)&&(await I({operation:"daemon.handover"}),process.exit(0)),process.pid===1&&await Nr();const r=await Mr({waitForJoinLock:!0});b.fresh(()=>{u({level:"info",event:"daemon.handover.completed",fields:{predecessor_pid:process.pid,successor_pid:r.pid??r.existing_pid??null,spawned:r.started}})})}catch(r){b.fresh(()=>{u({level:"error",event:"daemon.handover.successor_failed",error:r})});const e=r instanceof Error?r.message:String(r);process.stderr.write(`[crtrd] handover successor failed to start: ${e}
11
+ `),process.exit(1)}process.exit(0)}s(ge,"performHandover");function R(){if(S!==void 0)return S;const r=ve();return S=r,r.then(()=>{S===r&&(S=void 0)},()=>{S===r&&(S=void 0)}),r}s(R,"requestCleanup");async function K(r){try{await R(),process.exit(0)}catch(e){process.exitCode=1;const c=e instanceof Error?e.stack??`${e.name}: ${e.message}`:String(e);process.stderr.write(`[crtrd] ${r} cleanup failed:
12
12
  ${c}
13
- `),process.exit(1)}}s(W,"handleSignal");function U(){W("SIGINT")}s(U,"onSigint");function K(){W("SIGTERM")}s(K,"onSigterm");function J(){E().catch(()=>{})}s(J,"onExit");function N(r){f&&(w=w.then(r).then(()=>{},e=>{_.fresh(()=>{u({level:"error",event:"daemon.lane_job.failed",error:e})})}))}s(N,"enqueue");function V(){!f||v||(pe.check()&&I&&(u({level:"info",event:"daemon.resume.detected"}),b(I().catch(()=>{}))),v=!0,N(async()=>{if(v=!1,!!f)try{await yt(Date.now(),j)}catch(r){_.fresh(()=>{u({level:"error",event:"daemon.tick.failed",error:r})})}}))}s(V,"enqueueTick");let C=null;async function we(){if(C!==null)throw{status:500,code:"admission_failed",message:`daemon admission previously failed: ${C instanceof Error?C.message:String(C)}`};if(h.phase==="ready")return{admitted:!1,already_admitted:!0,startup_phase:"ready"};if(h.phase!=="prepared"&&h.phase!=="recovering")throw{status:409,code:"not_prepared",message:h.message};const r=h.phase==="prepared";try{return await M(),h={phase:"ready",message:"",blockerPaths:[]},r?{admitted:!0,already_admitted:!1,startup_phase:"ready"}:{admitted:!1,already_admitted:!0,startup_phase:"ready"}}catch(e){throw C=e,{status:500,code:"admission_failed",message:`daemon admission failed: ${e instanceof Error?e.message:String(e)}`}}}s(we,"admitStartup");async function ge(r){if(x!==null)throw{status:409,code:"migration_in_progress",message:`the canvas import is already running (${x})`};x="repair";try{const e=await Yr({dryRun:r.dryRun});if(r.dryRun||e.pending||h.phase!=="blocked")return{result:e,resumed:!1};if(t.prepare)return h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]},{result:e,resumed:!1};try{return await M(),h={phase:"ready",message:"",blockerPaths:[]},{result:e,resumed:!0}}catch(c){const g=c instanceof Error?c.message:String(c);return setTimeout(()=>{E().catch(()=>{})},1e3),{result:e,resumed:!1,resumeError:g}}}finally{x=null}}s(ge,"repairMigrations");async function ye(){o=t.intervalMs??sr;const r=a.token;l=new Wr({epoch:r,enqueue:N}),xr(l),h={phase:"recovering",message:"daemon startup recovery is running and normal services are unavailable",blockerPaths:[]},_.fresh(()=>{u({level:"info",event:"daemon.started",fields:{pid:process.pid,interval_ms:o}})});const e=Ar();if(Nr(e),_.fresh(()=>{u({level:"info",event:"daemon.install_id.resolved",fields:{install_id:e}})}),_.fresh(()=>{let c;try{c=ir()}catch{return}c!==null&&u({level:"warn",event:"daemon.startup.warning",fields:{stage:"termrender",warning:c.slice(0,2048)}})}),await _.fresh(async()=>{await or(),await rr(),tr(Date.now()),nr(Date.now())}),_.fresh(()=>{const{reconcile:c,migrated:g}=$r(vr);c.changed&&u({level:"info",event:"daemon.boot_change.reconciled",fields:{cleared_count:c.cleared.length,same_boot_proven:c.sameBootProven}}),g.length>0&&u({level:"info",event:"daemon.pid_identity.migrated",fields:{migrated_count:g.length}})}),_.fresh(()=>{Er();for(const c of Ur){const g=c.apply();g.changed&&u({level:"info",event:"daemon.boot_migration.applied",fields:{migration:c.id,...g.summary}})}}),b(Xe()),I){const c=s(()=>{b(I().catch(()=>{})),b(_e().then(()=>{},g=>{u({level:"warn",event:"daemon.peer.status_failed",error:g})}))},"refresh");b(Qe().catch(()=>{})),b(Ye().then(({completion:g})=>g).catch(g=>{u({level:"error",event:"daemon.grants.sweep_failed",error:g})})),c(),D=setInterval(c,Ve*1e3),D.unref()}N(()=>{zr(),jr()}),d=setInterval(V,o),V()}s(ye,"completeStartup");function M(){return me??=ye().catch(r=>{throw h={phase:"initializing",message:"daemon startup recovery failed and normal services are unavailable",blockerPaths:[]},r})}s(M,"resumeStartup");try{process.on("SIGINT",U),process.on("SIGTERM",K),process.on("exit",J),Jr(),m!==null&&u({level:"info",event:"daemon.joined_owner.applied",fields:{owner:m.owner,rows:m.rows}}),$=await Pe();const r=a.token;Tr({epoch:r,requestHandover:he,registerDetached:b,startupBlock:fe,admitStartup:we,repairMigrations:ge}),ce(),L=je({tcp:t.tcp}),await L.ready;const e=er("user").runtime;e?.issuer?(I=Je(e.issuer,b),Ue(c=>Ke().appended(c))):We(),e?.unsandboxed===!0&&e.listener!==void 0&&u({level:"warn",event:"daemon.app_listener.refused_unsandboxed",fields:{reason:"runtime.unsandboxed is set; runtime.listener is ignored"}}),e!==void 0&&e.listener!==void 0&&e.unsandboxed!==!0&&(T=ze(e,()=>Ze(e.issuer.replace(/\/$/,"")),I),await T.ready);try{await Vr()}catch(c){if(x=null,c instanceof Qr)return h={phase:"blocked",message:c.message,blockerPaths:c.blockerPaths},E;throw c}process.platform==="linux"&&k(R)&&(Ne(),await Ae("app:terminal"));try{t.prepare?h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]}:(await M(),h={phase:"ready",message:"",blockerPaths:[]})}finally{x=null}return E}catch(r){try{await E()}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}}s(_t,"runDaemon");var Un=_t;export{F as brokerCommandDeclaresCanvasHome,H as brokerCommandEpoch,Un as default,_t as runDaemon,yt as superviseTick,it as teardownFleet};
13
+ `),process.exit(1)}}s(K,"handleSignal");function J(){K("SIGINT")}s(J,"onSigint");function V(){K("SIGTERM")}s(V,"onSigterm");function Q(){R().catch(()=>{})}s(Q,"onExit");function O(r){f&&(g=g.then(r).then(()=>{},e=>{b.fresh(()=>{u({level:"error",event:"daemon.lane_job.failed",error:e})})}))}s(O,"enqueue");function Y(){!f||v||(fe.check()&&E&&(u({level:"info",event:"daemon.resume.detected"}),_(E().catch(()=>{}))),v=!0,O(async()=>{if(v=!1,!!f)try{await kt(Date.now(),W)}catch(r){b.fresh(()=>{u({level:"error",event:"daemon.tick.failed",error:r})})}}))}s(Y,"enqueueTick");let L=null;async function ye(){if(L!==null)throw{status:500,code:"admission_failed",message:`daemon admission previously failed: ${L instanceof Error?L.message:String(L)}`};if(h.phase==="ready")return{admitted:!1,already_admitted:!0,startup_phase:"ready"};if(h.phase!=="prepared"&&h.phase!=="recovering")throw{status:409,code:"not_prepared",message:h.message};const r=h.phase==="prepared";try{return await G(),h={phase:"ready",message:"",blockerPaths:[]},r?{admitted:!0,already_admitted:!1,startup_phase:"ready"}:{admitted:!1,already_admitted:!0,startup_phase:"ready"}}catch(e){throw L=e,{status:500,code:"admission_failed",message:`daemon admission failed: ${e instanceof Error?e.message:String(e)}`}}}s(ye,"admitStartup");async function Se(r){if(P!==null)throw{status:409,code:"migration_in_progress",message:`the canvas import is already running (${P})`};P="repair";try{const e=await rt({dryRun:r.dryRun});if(r.dryRun||e.pending||h.phase!=="blocked")return{result:e,resumed:!1};if(t.prepare)return h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]},{result:e,resumed:!1};try{return await G(),h={phase:"ready",message:"",blockerPaths:[]},{result:e,resumed:!0}}catch(c){const w=c instanceof Error?c.message:String(c);return setTimeout(()=>{R().catch(()=>{})},1e3),{result:e,resumed:!1,resumeError:w}}}finally{P=null}}s(Se,"repairMigrations");async function be(){o=t.intervalMs??lr;const r=a.token;l=new Vr({epoch:r,enqueue:O}),Cr(l),h={phase:"recovering",message:"daemon startup recovery is running and normal services are unavailable",blockerPaths:[]},b.fresh(()=>{u({level:"info",event:"daemon.started",fields:{pid:process.pid,interval_ms:o}})});const e=Gr();if(Fr(e),b.fresh(()=>{u({level:"info",event:"daemon.install_id.resolved",fields:{install_id:e}})}),b.fresh(()=>{let c;try{c=dr()}catch{return}c!==null&&u({level:"warn",event:"daemon.startup.warning",fields:{stage:"termrender",warning:c.slice(0,2048)}})}),await b.fresh(async()=>{await cr(),await ir(),sr(Date.now()),ar(Date.now())}),b.fresh(()=>{const{reconcile:c,migrated:w}=Or(Sr);c.changed&&u({level:"info",event:"daemon.boot_change.reconciled",fields:{cleared_count:c.cleared.length,same_boot_proven:c.sameBootProven}}),w.length>0&&u({level:"info",event:"daemon.pid_identity.migrated",fields:{migrated_count:w.length}})}),b.fresh(()=>{Ir();for(const c of Qr){const w=c.apply();w.changed&&u({level:"info",event:"daemon.boot_migration.applied",fields:{migration:c.id,...w.summary}})}}),_(tr()),E){const c=s(()=>{_(E().catch(()=>{})),_(Ee().then(w=>C?.observe(w)).then(()=>{},w=>{u({level:"warn",event:"daemon.peer.status_failed",error:w})}))},"refresh");_(er().catch(()=>{})),_(rr().then(({completion:w})=>w).catch(w=>{u({level:"error",event:"daemon.grants.sweep_failed",error:w})})),c(),D=setInterval(c,Ze*1e3),D.unref(),M=ke({joined:!0,register:_})}O(()=>{Jr(),Kr()}),d=setInterval(Y,o),Y()}s(be,"completeStartup");function G(){return ue??=be().catch(r=>{throw h={phase:"initializing",message:"daemon startup recovery failed and normal services are unavailable",blockerPaths:[]},r})}s(G,"resumeStartup");try{process.on("SIGINT",J),process.on("SIGTERM",V),process.on("exit",Q),Xr(),m!==null&&u({level:"info",event:"daemon.joined_owner.applied",fields:{owner:m.owner,rows:m.rows}}),A=await Le();const r=a.token;$r({epoch:r,requestHandover:we,registerDetached:_,startupBlock:he,admitStartup:ye,repairMigrations:Se}),le(),$=Ke({tcp:t.tcp}),await $.ready;const e=or("user").runtime;e?.issuer?(E=Xe(e.issuer,_),e.unsandboxed===!0&&(C=Re({syncGrants:E})),Qe(c=>Ye().appended(c))):Ve(),e?.unsandboxed===!0&&e.listener!==void 0&&u({level:"warn",event:"daemon.app_listener.refused_unsandboxed",fields:{reason:"runtime.unsandboxed is set; runtime.listener is ignored"}}),e!==void 0&&e.listener!==void 0&&e.unsandboxed!==!0&&(T=Je(e,()=>nr(e.issuer.replace(/\/$/,"")),E),await T.ready);try{await Zr()}catch(c){if(P=null,c instanceof et)return h={phase:"blocked",message:c.message,blockerPaths:c.blockerPaths},R;throw c}process.platform==="linux"&&k(x)&&(Fe(),await Ge("app:terminal"));try{t.prepare?h={phase:"prepared",message:"daemon storage is prepared and normal services are unavailable until admission",blockerPaths:[]}:(await G(),h={phase:"ready",message:"",blockerPaths:[]})}finally{P=null}return R}catch(r){try{await R()}catch(e){throw new AggregateError([r,e],"daemon startup failed and cleanup also failed")}throw r}}s(Rt,"runDaemon");var Xn=Rt;export{B as brokerCommandDeclaresCanvasHome,j as brokerCommandEpoch,Xn as default,Rt as runDaemon,kt as superviseTick,dt as teardownFleet};
@@ -0,0 +1,69 @@
1
+ import { ApiError } from '../../api/errors.js';
2
+ import type { DatabaseSync } from 'node:sqlite';
3
+ import type { ObjectRow } from '../../core/graph/types.js';
4
+ import { type Envelope, type EnvelopeCaller } from '../../core/peer/envelope.js';
5
+ import { type PeerRequest } from '../../core/peer/client.js';
6
+ import { type HomeDeps } from '../../core/peer/home.js';
7
+ import { peerCall } from '../../core/peer/client.js';
8
+ import type { RequestContext, RequestPrincipal } from '../api/router.js';
9
+ export interface MachineLabel {
10
+ id: string;
11
+ name: string;
12
+ }
13
+ export interface ForwardDeps extends Omit<HomeDeps, 'call'> {
14
+ /** Without an envelope the call is the machine's own (`peer.locate`); with one it is made for the caller. */
15
+ call(machine: string, request: PeerRequest, envelope?: Envelope, signal?: AbortSignal): ReturnType<typeof peerCall>;
16
+ envelope(principal: EnvelopeCaller): Envelope;
17
+ label(machine: string): Promise<MachineLabel>;
18
+ }
19
+ export declare const defaultForwardDeps: ForwardDeps;
20
+ /** The deps the handlers use; tests replace `current`. */
21
+ export declare const forwarding: {
22
+ current: ForwardDeps;
23
+ };
24
+ export type ForwardOutcome = {
25
+ kind: 'local';
26
+ } | {
27
+ kind: 'unknown';
28
+ unreachable: string[];
29
+ } | {
30
+ kind: 'remote';
31
+ status: number;
32
+ body: unknown;
33
+ machine: MachineLabel;
34
+ };
35
+ /** The callers whose by-id verbs may leave this machine: a person, a broker (a node), a cron. */
36
+ export declare const originates: (principal: RequestPrincipal) => principal is EnvelopeCaller;
37
+ /** Relay a non-2xx peer answer as the same API error. */
38
+ export declare function relayError(status: number, body: unknown): ApiError;
39
+ /** The error a verb throws when a machine could not answer. Never `not_found`. */
40
+ export declare function unknownError(id: string, unreachable: string[]): ApiError;
41
+ /** Add `machine` to an object body. */
42
+ export declare const withMachine: (body: unknown, machine: MachineLabel) => unknown;
43
+ /**
44
+ * Resolve where `id` lives and, when it is another machine, make `request` there as the caller. `request` is built from the home
45
+ * machine's id only for logging; it is the same call whatever machine answers.
46
+ */
47
+ export declare function forwardVerb(ctx: RequestContext, id: string, request: PeerRequest): Promise<ForwardOutcome>;
48
+ /** The envelope a forwarded call carried, rebuilt from its verified principal. */
49
+ export declare function envelopeOfForwarded(principal: Extract<RequestPrincipal, {
50
+ class: 'forwarded';
51
+ }>): Envelope;
52
+ /** The query a forwarded request carries: the caller's, without the `machine` hint (a hint names where to go, not what to ask). */
53
+ export declare function queryWithoutMachine(ctx: RequestContext): Record<string, string>;
54
+ /** On the home machine: the stub of the node a forwarded app call is made for (created or refreshed from the verified envelope), or null
55
+ * for a person actor, an unanchored app, or a local caller. */
56
+ export declare function callerStub(db: DatabaseSync, ctx: RequestContext): ObjectRow | null;
57
+ /** On the calling machine: the node making a call, when the caller is a node (a broker), else null. */
58
+ export declare function callerNode(db: DatabaseSync, ctx: RequestContext): ObjectRow | null;
59
+ /** On the calling machine: the stub of an object the home machine answered for. */
60
+ export declare function remoteStub(db: DatabaseSync, id: string, type: string, machine: string, ref?: {
61
+ kind?: string | null;
62
+ name?: string;
63
+ preview?: string;
64
+ owner?: {
65
+ type: string;
66
+ handle: string;
67
+ external_id?: string | null;
68
+ } | null;
69
+ }): ObjectRow;
@@ -0,0 +1 @@
1
+ var x=Object.defineProperty;var r=(e,n)=>x(e,"name",{value:n,configurable:!0});import{ApiError as f}from"../../api/errors.js";import{openDb as _}from"../../core/canvas/db.js";import{getById as v,nodeObject as $}from"../../core/graph/objects.js";import{envelopeFor as g}from"../../core/peer/envelope.js";import{isUnreachable as j}from"../../core/peer/client.js";import{machineReach as D}from"../../core/peer/directory.js";import{defaultHomeDeps as O,homeOf as S,relocate as q}from"../../core/peer/home.js";import{ensureStub as k,rehomeStub as A,stubOf as E}from"../../core/peer/stubs.js";import{peerCall as F}from"../../core/peer/client.js";const C={...O,call:r((e,n,o,t)=>F(e,n,{...o===void 0?{}:{envelope:o},...t===void 0?{}:{signal:t}}),"call"),envelope:r(e=>g(e),"envelope"),label:r(async e=>{try{const n=await D(e);return{id:e,name:n?.name??e}}catch{return{id:e,name:e}}},"label")},M={current:C},N=r(e=>e.class==="person"||e.class==="broker"||e.class==="cron","originates"),m=r(e=>{const n=e?.error;return typeof n=="object"&&n!==null&&typeof n.code=="string"?n.code:null},"errorCode");function L(e,n){const o=n?.error;return new f(e,o?.code??"error",o?.message??`the home machine answered ${e}`,o?.details)}r(L,"relayError");function P(e,n){return new f(503,"runtime_unreachable",`No machine of yours answers for ${e} (unreachable: ${n.join(", ")}).`,{id:e,unreachable:n})}r(P,"unknownError");const Q=r((e,n)=>e!==null&&typeof e=="object"&&!Array.isArray(e)?{...e,machine:n}:e,"withMachine");async function T(e,n,o){const t=M.current,a=_(),u=v(a,n),l=u===null?null:E(a,u),w=e.principal;if(!N(w)){if(l!==null)throw new f(404,"not_here",`${n} is not homed on this machine (it is a stub of ${l.home_machine})`,{id:n,home_machine:l.home_machine});return{kind:"local"}}if(u!==null&&l===null)return{kind:"local"};const b={...t,call:r((s,d)=>t.call(s,d),"call")};let i=await S(a,n,{hint:e.query.get("machine"),deps:b});for(let s=0;s<2;s++){if(i.where==="nowhere"||i.where==="here"){if(l!==null)throw new f(404,"not_found",`${n} is no longer on any machine of yours`,{id:n});return{kind:"local"}}if(i.where==="unknown")return{kind:"unknown",unreachable:i.unreachable};const d=i.machine,c=await t.call(d,o,t.envelope(w)),p=await t.label(d);if(j(c))return{kind:"unknown",unreachable:[`${p.name} (${c.unreachable.reason})`]};const y=i.source!=="locate";if(c.status===404&&s===0&&(m(c.body)==="not_here"||y&&m(c.body)==="not_found")){const h=await q(a,n,d,b);if(h.where==="machine"){i=h;continue}if(h.where==="unknown")return{kind:"unknown",unreachable:h.unreachable};if(m(c.body)==="not_found")return{kind:"remote",status:c.status,body:c.body,machine:p};if(l!==null)throw new f(404,"not_found",`${n} is no longer on any machine of yours`,{id:n});return{kind:"local"}}return{kind:"remote",status:c.status,body:c.body,machine:p}}return{kind:"unknown",unreachable:[]}}r(T,"forwardVerb");function W(e){if(e.actor.kind==="person")return{sub:e.sub,act:{sub:e.via}};const{app:n,node:o,node_scopes:t,place:a}=e.actor;return{sub:e.sub,act:{sub:e.via,act:{sub:n,node:o,node_scopes:t,place:a}}}}r(W,"envelopeOfForwarded");function X(e){const n={};for(const[o,t]of e.query)o!=="machine"&&(n[o]=t);return n}r(X,"queryWithoutMachine");function Y(e,n){const o=n.principal;return o.class!=="forwarded"||o.actor.kind!=="app"||o.actor.node===null?null:k(e,o.actor.node,"node",o.via,W(o))}r(Y,"callerStub");function Z(e,n){return n.principal.class==="broker"?$(e,n.principal.node):null}r(Z,"callerNode");function ee(e,n,o,t,a){A(e,n,t);const u=a?.owner,l=u&&u.type==="app"?{id:"",type:"app",scheme:"app",external_id:u.external_id??(u.handle.startsWith("app:")?u.handle:`app:${u.handle}`),handle:u.handle}:null;return k(e,n,o,t,void 0,a===void 0?void 0:{id:n,type:o,kind:a.kind??null,name:a.name??n,...a.preview===void 0?{}:{preview:a.preview},owner:l})}r(ee,"remoteStub");export{Z as callerNode,Y as callerStub,C as defaultForwardDeps,W as envelopeOfForwarded,T as forwardVerb,M as forwarding,N as originates,X as queryWithoutMachine,L as relayError,ee as remoteStub,P as unknownError,Q as withMachine};
@@ -1,2 +1,7 @@
1
- import type { RouteTable } from '../api/router.js';
1
+ import type { RouteHandler, RouteTable } from '../api/router.js';
2
+ export declare const peerEvents: RouteHandler;
3
+ export declare const peerClaim: RouteHandler;
4
+ export declare const peerAck: RouteHandler;
5
+ /** `peer.locate`: "do you hold `id`?" -- `here` (a real local object), `moved` (a stub: the machine that homes it), or `no`. Reads nothing else. */
6
+ export declare const peerLocate: RouteHandler;
2
7
  export declare const peerRoutes: RouteTable;
@@ -1 +1 @@
1
- var n=Object.defineProperty;var o=(t,r)=>n(t,"name",{value:r,configurable:!0});import{ApiError as p}from"../../api/errors.js";const e=o(t=>()=>{throw new p(501,"not_implemented",`${t} is not implemented on this machine yet`)},"notImplemented"),d=[{method:"POST",pattern:"/v1/peer/events",handler:e("peer.events")},{method:"POST",pattern:"/v1/peer/outbox/claim",handler:e("peer.outbox.claim")},{method:"POST",pattern:"/v1/peer/outbox/ack",handler:e("peer.outbox.ack")},{method:"POST",pattern:"/v1/peer/locate",handler:e("peer.locate")}];export{d as peerRoutes};
1
+ var u=Object.defineProperty;var a=(e,t)=>u(e,"name",{value:t,configurable:!0});import{ApiError as c}from"../../api/errors.js";import{withCanvasWrite as l}from"../../core/canvas/db.js";import{getById as d}from"../../core/graph/objects.js";import{stubOf as v}from"../../core/peer/stubs.js";import{ackFor as y,claimFor as f,ingestPeerEvents as h,MAX_EVENTS_PER_CALL as p}from"../../core/peer/ingest.js";function m(e){if(e.principal.class!=="machine")throw new c(403,"permission_error","peer routes are for a machine");return e.principal.via}a(m,"via");const o=a(e=>e!==null&&typeof e=="object"&&!Array.isArray(e)?e:{},"fields"),s=a(e=>typeof e=="string"&&e!=="","text");function w(e){const t=o(e),r=o(t.object);return s(t.key)&&s(t.message_id)&&s(t.event)&&typeof t.level=="number"&&s(r.id)&&s(r.type)&&typeof r.name=="string"&&(t.actor===null||s(o(t.actor).id)&&s(o(t.actor).type))&&o(t.attrs)===t.attrs}a(w,"validEvent");const b=a(e=>{const t=m(e),r=o(e.body).events;if(!Array.isArray(r))throw new c(400,"invalid_request","events must be an array");if(r.length>p)throw new c(400,"invalid_request",`at most ${p} events per call`);const n=r.findIndex(i=>!w(i));if(n>=0)throw new c(400,"invalid_request",`events[${n}] is not a well-formed event`);return{status:200,body:l(i=>h(i,t,r))}},"peerEvents"),_=a(e=>{const t=m(e),r=o(e.body).limit;return{status:200,body:{rows:l(i=>f(i,t,typeof r=="number"?r:50))}}},"peerClaim"),k=a(e=>{const t=m(e),r=o(e.body).acks;if(!Array.isArray(r)||r.some(n=>!s(o(n).key)||!s(o(n).claim_token)))throw new c(400,"invalid_request","acks must be an array of {key, claim_token}");return{status:200,body:{acked:l(n=>y(n,t,r))}}},"peerAck"),A=a(e=>{m(e);const t=o(e.body).id;if(!s(t))throw new c(400,"invalid_request","id must be a non-empty string");return l(r=>{const n=d(r,t);if(n===null)return{status:200,body:{answer:"no"}};const i=v(r,n);return{status:200,body:i===null?{answer:"here"}:{answer:"moved",machine:i.home_machine}}})},"peerLocate"),T=[{method:"POST",pattern:"/v1/peer/events",handler:b},{method:"POST",pattern:"/v1/peer/outbox/claim",handler:_},{method:"POST",pattern:"/v1/peer/outbox/ack",handler:k},{method:"POST",pattern:"/v1/peer/locate",handler:A}];export{k as peerAck,_ as peerClaim,b as peerEvents,A as peerLocate,T as peerRoutes};
@@ -0,0 +1,8 @@
1
+ import { type IncomingMessage, type ServerResponse } from 'node:http';
2
+ export declare const peerSocketPath: () => string;
3
+ export interface LocalIngress {
4
+ readonly socketPath: string;
5
+ close(): Promise<void>;
6
+ }
7
+ /** Bind `socketPath` (replacing a stale file) and serve every request with `serve`. Resolves once listening and chmodded 0600. */
8
+ export declare function startLocalIngress(serve: (req: IncomingMessage, res: ServerResponse) => Promise<void>, socketPath?: string): Promise<LocalIngress>;
@@ -0,0 +1 @@
1
+ var f=Object.defineProperty;var i=(c,r)=>f(c,"name",{value:r,configurable:!0});import{chmodSync as s,existsSync as t,unlinkSync as m}from"node:fs";import{createServer as l}from"node:http";import{join as p}from"node:path";import{runtimeRoot as a}from"../../core/layout.js";const y=i(()=>p(a(),"peer.sock"),"peerSocketPath");async function x(c,r=y()){const e=l((n,o)=>{c(n,o).catch(()=>{o.headersSent||o.writeHead(500),o.end()})});try{t(r)&&m(r)}catch{}return await new Promise((n,o)=>{e.once("error",o),e.listen(r,()=>{e.off("error",o),n()})}),s(r,384),{socketPath:r,close:i(()=>new Promise(n=>{e.close(()=>{try{t(r)&&m(r)}catch{}n()}),e.closeAllConnections()}),"close")}}i(x,"startLocalIngress");export{y as peerSocketPath,x as startLocalIngress};
@@ -0,0 +1,38 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { type PeerRequest, type PeerResult } from '../../core/peer/client.js';
3
+ import { type Machine } from '../../core/peer/directory.js';
4
+ export declare const STABLE_BACKOFF_MS: {
5
+ readonly first: 1000;
6
+ readonly max: number;
7
+ };
8
+ /** A relay-reach peer is retried for this long after its first failure, then left for its own pull. */
9
+ export declare const RELAY_RETRY_WINDOW_MS: number;
10
+ export declare const PULL_INTERVAL_MS: number;
11
+ export interface OutboxDeps {
12
+ db(): DatabaseSync;
13
+ call(machineId: string, request: PeerRequest): Promise<PeerResult>;
14
+ reach(machineId: string): Promise<Machine | null>;
15
+ machines(): Promise<Machine[]>;
16
+ self(): string;
17
+ now(): number;
18
+ }
19
+ export declare const liveOutboxDeps: OutboxDeps;
20
+ /** The wait before attempt number `attempts` (already made) retries: 1 s doubling to 10 min. */
21
+ export declare const stableBackoffMs: (attempts: number) => number;
22
+ export interface OutboxLane {
23
+ /** Push every due row. Called from the daemon's tick. */
24
+ run(): Promise<void>;
25
+ /** Push what is pending to one peer now (a row was just enqueued). */
26
+ flush(peer: string): Promise<void>;
27
+ /** Claim and apply what every other machine holds for this one. Only a relay-reach machine needs it. */
28
+ pull(): Promise<number>;
29
+ }
30
+ export declare function createOutboxLane(deps: OutboxDeps): OutboxLane;
31
+ /** Start the lane in the daemon: push on enqueue and on a short timer, pull on relay connect, at boot and every 5 minutes.
32
+ * `joined` is false for a runtime with no directory identity (nothing to push to or pull from). Returns the stop function. */
33
+ export declare function startPeerOutbox(opts: {
34
+ joined: boolean;
35
+ register(work: Promise<void>): void;
36
+ deps?: OutboxDeps;
37
+ tickMs?: number;
38
+ }): () => void;
@@ -0,0 +1 @@
1
+ var T=Object.defineProperty;var f=(e,d)=>T(e,"name",{value:d,configurable:!0});import{openDb as I,withCanvasWrite as L}from"../../core/canvas/db.js";import{emitEvent as R}from"../../core/events/emit.js";import{isUnreachable as A,peerCall as j}from"../../core/peer/client.js";import{listMachines as B,machineReach as C,normalizeMachineId as k,ownMachineId as D}from"../../core/peer/directory.js";import{relayConnected as U}from"../../core/peer/events.js";import{ingestPeerEvents as F}from"../../core/peer/ingest.js";import{dueRows as N,expireOld as W,markDone as q,markExpired as O,markRetry as x,markWaiting as S,onPeerEnqueued as X,pendingFor as Y,pruneOutbox as z,OUTBOX_MAX_AGE_MS as G}from"../../core/peer/outbox-store.js";const g={first:1e3,max:10*6e4},E=5*6e4,H=5*6e4,P=50,J=20,K={db:I,call:f((e,d)=>j(e,d),"call"),reach:C,machines:B,self:D,now:Date.now},M=f(e=>Math.min(g.first*2**Math.max(0,e-1),g.max),"stableBackoffMs");function V(e){const d=new Set;async function p(s,a,o){const t=e.db(),n=e.now(),l=await e.call(s,{method:"POST",path:"/v1/peer/events",body:{events:a.map(r=>JSON.parse(r.payload_json))}}),m=f(r=>{for(const c of a){const h=c.attempts+1,_=c.first_failed_at??n;o&&r?S(t,c.id,h):o?n-_>=E?S(t,c.id,h):x(t,c.id,h,n+Math.min(M(h),E-(n-_)),_):n-_>=G?O(t,c.id):x(t,c.id,h,n+M(h),_)}},"fail");if(A(l)){m(l.unreachable.reason==="machine_offline");return}const i=l.body;if(l.status!==200||i===null||!Array.isArray(i.accepted)){m(!1);return}const y=new Set(i.accepted),b=new Set((Array.isArray(i.rejected)?i.rejected:[]).map(r=>r.key));for(const r of a)y.has(r.idempotency_key)?q(t,r.id):b.has(r.idempotency_key)?O(t,r.id):x(t,r.id,r.attempts+1,n+M(r.attempts+1),r.first_failed_at??n)}f(p,"sendBatch");async function w(s){const a=s.filter(o=>!d.has(o.id));for(const o of a)d.add(o.id);try{const o=new Map;for(const t of a)o.set(t.peer,[...o.get(t.peer)??[],t]);await Promise.all([...o].map(async([t,n])=>{const m=(await e.reach(t).catch(()=>null))?.reach.kind==="relay";for(let i=0;i<n.length;i+=P)await p(t,n.slice(i,i+P),m)}))}finally{for(const o of a)d.delete(o.id)}}f(w,"pushRows");let u=0;return{async run(){const s=e.db(),a=e.now();a-u>=60*6e4&&(u=a,W(s,a),z(s,a)),await w(N(s,a))},flush:f(s=>w(Y(e.db(),s,e.now())),"flush"),async pull(){const s=e.self(),a=(await e.machines()).filter(t=>k(t.machine_id)!==s&&t.reach.kind==="stable");let o=0;for(const t of a){const n=k(t.machine_id);for(let l=0;l<J;l++){const m=await e.call(n,{method:"POST",path:"/v1/peer/outbox/claim",body:{limit:100}}),i=!A(m)&&m.status===200?m.body?.rows:void 0;if(!Array.isArray(i)||i.length===0)break;const y=L(c=>F(c,n,i.map(h=>h.payload)));o+=y.accepted.length;const b=new Set([...y.accepted,...y.rejected.map(c=>c.key)]),r=i.filter(c=>b.has(c.key)).map(c=>({key:c.key,claim_token:c.claim_token}));if(r.length>0&&await e.call(n,{method:"POST",path:"/v1/peer/outbox/ack",body:{acks:r}}),i.length<100)break}}return o}}}f(V,"createOutboxLane");function re(e){if(!e.joined)return()=>{};const d=e.deps??K,p=V(d),w=f(n=>l=>{R({level:"warn",event:`daemon.peer.${n}_failed`,error:l})},"quiet"),u=f((n,l)=>e.register(n().then(()=>{},w(l))),"guarded"),v=X(n=>u(()=>p.flush(n),"outbox_flush")),s=setInterval(()=>u(()=>p.run(),"outbox_run"),e.tickMs??5e3);s.unref();const a=f(()=>u(async()=>{(await d.machines()).find(l=>k(l.machine_id)===d.self())?.reach.kind==="relay"&&await p.pull()},"outbox_pull"),"pull"),o=U.on(a),t=setInterval(a,H);return t.unref(),a(),()=>{v(),o(),clearInterval(s),clearInterval(t)}}f(re,"startPeerOutbox");export{H as PULL_INTERVAL_MS,E as RELAY_RETRY_WINDOW_MS,g as STABLE_BACKOFF_MS,V as createOutboxLane,K as liveOutboxDeps,M as stableBackoffMs,re as startPeerOutbox};
@@ -0,0 +1,35 @@
1
+ import { WebSocket } from 'ws';
2
+ import { type TokenCache } from '../../core/peer/token.js';
3
+ export declare const BACKOFF_MIN_MS = 1000;
4
+ export declare const BACKOFF_MAX_MS = 60000;
5
+ /** A `token` frame goes this long before the current token's `exp`. */
6
+ export declare const TOKEN_REFRESH_LEAD_MS = 60000;
7
+ /** The relay pings every 20 s; a socket that has said nothing for this long is dead. */
8
+ export declare const SILENCE_LIMIT_MS = 60000;
9
+ export declare const HELLO_TIMEOUT_MS = 10000;
10
+ export interface RelayClientOptions {
11
+ /** The directory (`iss` of the machine tokens). */
12
+ issuer: string;
13
+ /** The relay origin from the machine's reach (`https://relay.northlight.ai`); also the token audience. */
14
+ relayOrigin: string;
15
+ /** The unix socket the private ingress listens on. */
16
+ socketPath: string;
17
+ tokens?: TokenCache;
18
+ backoffMinMs?: number;
19
+ backoffMaxMs?: number;
20
+ refreshLeadMs?: number;
21
+ silenceLimitMs?: number;
22
+ helloTimeoutMs?: number;
23
+ /** Test seam: the WebSocket constructor. */
24
+ connect?: (url: string) => WebSocket;
25
+ }
26
+ export interface RelayClient {
27
+ /** True between `hello_ok` and the socket closing. */
28
+ connected(): boolean;
29
+ /** Drop the socket (the machine slept; it is probably dead) and dial again at once. */
30
+ reconnectNow(): void;
31
+ stop(): Promise<void>;
32
+ }
33
+ /** `https://relay.northlight.ai` -> `wss://relay.northlight.ai/v1/connect`. */
34
+ export declare function relayConnectUrl(origin: string): string;
35
+ export declare function startRelayClient(serveOptions: RelayClientOptions): RelayClient;
@@ -0,0 +1 @@
1
+ var I=Object.defineProperty;var o=(_,e)=>I(_,"name",{value:e,configurable:!0});import{request as Y}from"node:http";import{decodeJwt as D}from"jose";import{WebSocket as R}from"ws";import{emitEvent as b}from"../../core/events/emit.js";import{relayConnected as q}from"../../core/peer/events.js";import{peerTokens as j}from"../../core/peer/token.js";import{parseServerFrame as z,RELAY_CLOSE as B,RELAY_MAX_BODY_BYTES as C,RELAY_PROTOCOL_VERSION as H}from"./relay-protocol.js";const J=1e3,K=6e4,U=6e4,X=6e4,V=1e4,W=["authorization","crtr-envelope","crtr-version","content-type"];function G(_){const e=new URL("/v1/connect",_);return e.protocol=e.protocol==="http:"?"ws:":"wss:",e.toString()}o(G,"relayConnectUrl");const Q=o(_=>{const e={host:"peer.local"};for(const E of W){const p=_[E];p!==void 0&&(e[E]=p)}return e},"pick");function se(_){const e={backoffMinMs:J,backoffMaxMs:K,refreshLeadMs:U,silenceLimitMs:X,helloTimeoutMs:V,tokens:j,connect:o(r=>new R(r,{maxPayload:Math.ceil(C*4/3)+65536}),"connect"),..._},E=G(e.relayOrigin);let p=!1,T,O=!1,k=e.backoffMinMs,m,v=0,L=!1;const h=new Map;function A(){if(m=void 0,p)return;const r=++v;(async()=>{let i;try{i=await e.tokens.get(e.issuer,e.relayOrigin)}catch(n){return b({level:"warn",event:"daemon.peer.relay_token_failed",error:n instanceof Error?n:new Error(String(n))}),x(r)}p||r!==v||N(i,r)})()}o(A,"dial");function x(r,i){if(p||r!==v||m!==void 0)return;const n=i??k;k=Math.min(k*2,e.backoffMaxMs),m=setTimeout(A,n),m.unref()}o(x,"schedule");function N(r,i){const n=e.connect(E);T=n;let l,a,f,u=!1,g=!1;const M=o(t=>{n.readyState===R.OPEN&&n.send(JSON.stringify(t))},"send"),d=o(()=>{clearTimeout(f),f=setTimeout(()=>{b({level:"warn",event:"daemon.peer.relay_silent"}),n.terminate()},e.silenceLimitMs),f.unref()},"heard"),w=o((t,c)=>{if(g)return;g=!0,clearTimeout(l),clearTimeout(a),clearTimeout(f);for(const[,S]of h)S.abort();h.clear(),T===n&&(T=void 0),O=!1,u?b({level:"info",event:"daemon.peer.relay_disconnected",fields:{code:t,reason:c}}):b({level:"warn",event:"daemon.peer.relay_connect_failed",fields:{code:t,reason:c}}),t===B.unauthorized&&e.tokens.drop(e.issuer,e.relayOrigin);const s=L;L=!1,x(i,s?0:t===B.replaced?e.backoffMaxMs:void 0)},"finish"),y=o(t=>{clearTimeout(a);const c=Math.max(0,t*1e3-e.refreshLeadMs-Date.now());a=setTimeout(()=>{e.tokens.drop(e.issuer,e.relayOrigin),e.tokens.get(e.issuer,e.relayOrigin).then(s=>{if(g)return;M({type:"token",token:s});const S=D(s).exp;y(typeof S=="number"&&S>t?S:t+1)},s=>{b({level:"warn",event:"daemon.peer.relay_refresh_failed",error:s instanceof Error?s:new Error(String(s))}),g||(clearTimeout(a),a=setTimeout(()=>y(t),5e3),a.unref())})},c),a.unref()},"planRefresh");n.on("open",()=>{M({type:"hello",protocol:H,token:r}),l=setTimeout(()=>n.terminate(),e.helloTimeoutMs),l.unref(),d()}),n.on("message",(t,c)=>{if(d(),c)return;const s=z(Array.isArray(t)?Buffer.concat(t).toString("utf8"):Buffer.from(t).toString("utf8"));s!==null&&F(s)}),n.on("close",(t,c)=>w(t,c.toString("utf8"))),n.on("error",t=>{b({level:"debug",event:"daemon.peer.relay_socket_error",error:t}),w(1006,"error");try{n.terminate()}catch{}});function F(t){switch(t.type){case"hello_ok":{clearTimeout(l),u=!0,O=!0,k=e.backoffMinMs,y(t.expires_at),b({level:"info",event:"daemon.peer.relay_connected",fields:{relay:e.relayOrigin,machine_id:t.machine_id}}),q.emit();return}case"ping":M({type:"pong"});return;case"pong":return;case"cancel":h.get(t.id)?.abort(),h.delete(t.id);return;case"request":{if(!u)return;const c=new AbortController;h.set(t.id,c),P(t,c.signal).then(s=>{h.delete(t.id),c.signal.aborted||M({type:"response",id:t.id,...s})});return}}}o(F,"handle")}o(N,"run");function P(r,i){const n=o((l,a,f)=>({status:l,headers:{"content-type":"application/json"},body_b64:Buffer.from(JSON.stringify({error:{code:a,message:f}})).toString("base64")}),"fail");return new Promise(l=>{const a=Buffer.from(r.body_b64,"base64"),f=Y({socketPath:e.socketPath,method:r.method,path:r.path,headers:{...Q(r.headers),"content-length":String(a.byteLength)},signal:i},u=>{const g=[];let M=0;u.on("data",d=>{if(M+=d.byteLength,M>C){l(n(502,"response_too_large","the answer is over 1 MiB")),f.destroy();return}g.push(d)}),u.on("end",()=>{const d={};for(const[w,y]of Object.entries(u.headers))typeof y=="string"?d[w]=y:Array.isArray(y)&&(d[w]=y.join(", "));l({status:Math.min(599,Math.max(200,u.statusCode??502)),headers:d,body_b64:Buffer.concat(g).toString("base64")})}),u.on("error",()=>l(n(502,"peer_ingress_failed","the peer ingress dropped the answer")))});f.on("error",()=>l(n(502,"peer_ingress_failed","the peer ingress could not be reached"))),f.end(a)})}return o(P,"replay"),A(),{connected:o(()=>O,"connected"),reconnectNow(){if(p)return;clearTimeout(m),m=void 0,k=e.backoffMinMs;const r=T;if(r!==void 0){L=!0,r.terminate();return}A()},async stop(){p=!0,clearTimeout(m),m=void 0,v++;for(const[,i]of h)i.abort();h.clear();const r=T;T=void 0,O=!1,r!==void 0&&await new Promise(i=>{const n=setTimeout(()=>{r.terminate(),i()},1e3);n.unref(),r.once("close",()=>{clearTimeout(n),i()});try{r.close(1e3,"going_away")}catch{r.terminate()}})}}}o(se,"startRelayClient");export{K as BACKOFF_MAX_MS,J as BACKOFF_MIN_MS,V as HELLO_TIMEOUT_MS,X as SILENCE_LIMIT_MS,U as TOKEN_REFRESH_LEAD_MS,G as relayConnectUrl,se as startRelayClient};
@@ -0,0 +1,52 @@
1
+ export declare const RELAY_PROTOCOL_VERSION = 1;
2
+ /** Largest request or response body, in bytes before base64. */
3
+ export declare const RELAY_MAX_BODY_BYTES = 1048576;
4
+ /** WebSocket close codes the relay uses. */
5
+ export declare const RELAY_CLOSE: {
6
+ readonly badFrame: 4000;
7
+ readonly unauthorized: 4001;
8
+ readonly replaced: 4002;
9
+ readonly tokenExpired: 4003;
10
+ };
11
+ export type RelayHeaders = Record<string, string>;
12
+ export type RelayClientFrame = {
13
+ type: 'hello';
14
+ protocol: number;
15
+ token: string;
16
+ } | {
17
+ type: 'token';
18
+ token: string;
19
+ } | {
20
+ type: 'response';
21
+ id: string;
22
+ status: number;
23
+ headers: RelayHeaders;
24
+ body_b64: string;
25
+ } | {
26
+ type: 'ping';
27
+ } | {
28
+ type: 'pong';
29
+ };
30
+ export type RelayServerFrame = {
31
+ type: 'hello_ok';
32
+ protocol: number;
33
+ machine_id: string;
34
+ expires_at: number;
35
+ heartbeat_s: number;
36
+ } | {
37
+ type: 'request';
38
+ id: string;
39
+ method: string;
40
+ path: string;
41
+ headers: RelayHeaders;
42
+ body_b64: string;
43
+ } | {
44
+ type: 'cancel';
45
+ id: string;
46
+ } | {
47
+ type: 'ping';
48
+ } | {
49
+ type: 'pong';
50
+ };
51
+ /** Parse one text frame from the relay; null when it is not a frame this client understands. */
52
+ export declare function parseServerFrame(text: string): RelayServerFrame | null;
@@ -0,0 +1 @@
1
+ var o=Object.defineProperty;var n=(t,e)=>o(t,"name",{value:e,configurable:!0});const c=1,l=1048576,s={badFrame:4e3,unauthorized:4001,replaced:4002,tokenExpired:4003},r=n(t=>typeof t=="string","isString"),p=n(t=>typeof t=="object"&&t!==null&&!Array.isArray(t)&&Object.values(t).every(r),"isHeaders");function u(t){let e;try{e=JSON.parse(t)}catch{return null}if(typeof e!="object"||e===null)return null;switch(e.type){case"hello_ok":return r(e.machine_id)&&typeof e.expires_at=="number"&&typeof e.heartbeat_s=="number"&&typeof e.protocol=="number"?e:null;case"request":return r(e.id)&&r(e.method)&&r(e.path)&&p(e.headers)&&r(e.body_b64)?e:null;case"cancel":return r(e.id)?{type:"cancel",id:e.id}:null;case"ping":return{type:"ping"};case"pong":return{type:"pong"};default:return null}}n(u,"parseServerFrame");export{s as RELAY_CLOSE,l as RELAY_MAX_BODY_BYTES,c as RELAY_PROTOCOL_VERSION,u as parseServerFrame};
@@ -0,0 +1,26 @@
1
+ import { type MachineStatus } from '../../core/peer/directory.js';
2
+ import { type PeerIngressContext } from './ingress.js';
3
+ import { type RelayClientOptions } from './relay-client.js';
4
+ export declare const SYNC_NOTICE_PATH = "/v1/runtime/sync-notice";
5
+ /** How often the clocks are compared to notice that the machine slept. */
6
+ export declare const RESUME_CHECK_MS = 5000;
7
+ export interface PeerRelayDeps {
8
+ syncGrants(options?: {
9
+ fresh?: boolean;
10
+ }): Promise<void>;
11
+ /** Overrides for tests. */
12
+ unsandboxed?: () => boolean;
13
+ issuer?: () => string | null;
14
+ jwksUrl?: () => string | undefined;
15
+ socketPath?: string;
16
+ client?: Partial<RelayClientOptions>;
17
+ ingress?: Partial<PeerIngressContext>;
18
+ resumeCheckMs?: number;
19
+ }
20
+ export interface PeerRelay {
21
+ /** Called after each directory status answer. Starts the relay when it qualifies; restarts it if the relay origin changed. */
22
+ observe(status: MachineStatus | null): Promise<void>;
23
+ running(): boolean;
24
+ stop(): Promise<void>;
25
+ }
26
+ export declare function createPeerRelay(deps: PeerRelayDeps): PeerRelay;
@@ -0,0 +1 @@
1
+ var U=Object.defineProperty;var o=(e,c)=>U(e,"name",{value:c,configurable:!0});import{JwksCache as E}from"@north-light/identity";import{randomUUID as N}from"node:crypto";import{readConfig as k}from"../../core/config.js";import{hasSyncedGrants as O}from"../../core/grants/mirror.js";import{emitEvent as _}from"../../core/events/emit.js";import{directoryIssuer as b,machineUrl as j,ownMachineId as R}from"../../core/peer/directory.js";import{RunError as l}from"../../core/runs/operations.js";import{createResumeDetector as H}from"../resume-detector.js";import{applySyncNotice as M,NoticeLedger as J}from"../api/sync-notice.js";import{runtimeOwner as T,servePeer as D}from"./ingress.js";import{startLocalIngress as G}from"./local-ingress.js";import{startRelayClient as L}from"./relay-client.js";const $="/v1/runtime/sync-notice",z=5e3;async function A(e,c,f){if(e.headersSent){e.destroy();return}e.writeHead(c,{"content-type":"application/json; charset=utf-8"}),e.end(JSON.stringify(f))}o(A,"answerJson");function oe(e){const c=e.unsandboxed??(()=>k("user").runtime?.unsandboxed===!0),f=e.issuer??b;let s;const S=new J;async function m(){const t=s;s=void 0,t!==void 0&&(clearInterval(t.resume),await t.client.stop(),await t.ingress.close())}o(m,"stopStarted");async function C(t,r){const i=new E(r,fetch,e.jwksUrl?e.jwksUrl():k("user").runtime?.jwksUrl),d={issuer:r,jwks:i,audience:j,owner:T,self:R,ready:o(()=>O(r.replace(/\/$/,"")),"ready"),...e.ingress},w=await G(o(async(a,u)=>{const x=(a.url??"").split("?",1)[0];if(a.method==="POST"&&x===$){a.resume();try{const n=d.audience();if(n===null||d.ready?.()===!1)throw new l(503,"runtime_starting","runtime_starting");await M(await S.accept(a.headers.authorization,i,r,n),e.syncGrants,N()),u.writeHead(204),u.end()}catch(n){const I=n instanceof l?n.status:500;await A(u,I,{error:{code:n instanceof l?n.code:"server_failure",message:n instanceof l?n.message:"sync notice failed"}})}return}await D(a,u,d)},"serve"),e.socketPath);let y;try{y=L({issuer:r,relayOrigin:t,socketPath:w.socketPath,...e.client})}catch(a){throw await w.close(),a}const P=H(),v=setInterval(()=>{P.check()&&(_({level:"info",event:"daemon.peer.relay_resume"}),y.reconnectNow())},e.resumeCheckMs??z);v.unref(),s={relay:t,ingress:w,client:y,resume:v,jwks:i},_({level:"info",event:"daemon.peer.relay_started",fields:{relay:t}})}o(C,"begin");let h=Promise.resolve(),p=!1;const g=o(t=>{const r=h.then(t);return h=r.catch(()=>{}),r},"serial");return{observe(t){return g(async()=>{if(p)return;const r=f();if(t===null)return;if(t.reach?.kind!=="relay"||!c()||r===null){await m();return}const i=t.reach.relay.replace(/\/$/,"");s?.relay!==i&&(await m(),await C(i,r))})},running:o(()=>s!==void 0,"running"),stop(){return p=!0,g(m)}}}o(oe,"createPeerRelay");export{z as RESUME_CHECK_MS,$ as SYNC_NOTICE_PATH,oe as createPeerRelay};
@@ -53,6 +53,8 @@ export interface InboxCardEntry {
53
53
  /** An already-resolved sender section supplied to the pure digest formatter. */
54
54
  export interface InboxCardSection {
55
55
  id: string;
56
+ /** The machine of the account the sender lives on, when it is not this one. */
57
+ machine?: string;
56
58
  entries: readonly InboxCardEntry[];
57
59
  }
58
60
  export interface GeneratedCard {
@@ -80,4 +82,10 @@ export declare function isHumanWordsEntry(entry: CardEntry): boolean;
80
82
  /** Explain how an inbox entry's ref is read without guessing its filesystem location. */
81
83
  export declare function formatInboxRefInstruction(ref: string): string;
82
84
  /** Format a complete inbox card from report bodies resolved by the caller. */
85
+ /** The machine a section's sender lives on, when its entries say it is another one (`data.from_machine`, set by the home side of a forwarded send). */
86
+ export declare function machineOfEntries(items: readonly {
87
+ data?: Record<string, unknown>;
88
+ }[]): {
89
+ machine?: string;
90
+ };
83
91
  export declare function formatInboxCard(sections: readonly InboxCardSection[]): string;
@@ -1,8 +1,8 @@
1
- var _=Object.defineProperty;var o=(e,t)=>_(e,"name",{value:t,configurable:!0});const T="crtr-context",E="crtr-situational-context",R="crtr-context-nudge",O="crtr-review-boundary",Y="You've stopped but you're not waiting on anyone and haven't finished. Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human send` if you are blocked or need the user.",I="This conversation has been idle with nothing left to wake it, so it is being concluded. This is your last turn. Use it to leave a trustworthy inheritance, not to restart or broaden the work. Do these four things now, then stop.\n\n1. Establish current truth. Check only state that may have changed outside the transcript and matters to resuming\u2014such as the working tree, a remote run, or an external decision. Do not start new work; perform only a quick check needed to avoid recording an unverified claim. If no mandate or work ever began, record that plainly in the inheritance and keep every artifact minimal.\n\n2. Put supporting material in documents you own (`crtr doc write`) only when it is needed to keep the inheritance concise. Rewrite existing living documents rather than leave superseded versions. Task state, identifiers, and recovery detail belong here, under this node, not under a longer-lived owner.\n\n3. Give a longer-lived owner (`--owner repo|profile|user`) only to a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr doc write -h`, search before writing (`crtr canvas search`), and choose the narrowest owner that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into such a document.\n\n4. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write for someone who has not seen this conversation: name the work in plain terms, say where it stands, and say what remains or is blocked. Put the current outcome in the first line; include only needed decisions and recovery handles, then stop.",$="Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.",L="The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.",S="Your previous turn stopped on a connection error reaching the model provider. Retrying now: continue from where you left off and retry the work that failed.",M="Your previous turn stopped on a provider fault. Continue from where you left off and retry the work that failed.",D="<crtr-review-approval>",j="</crtr-review-approval>",V="<model-fallback-recovery>",B="</model-fallback-recovery>",g="\n```";function F(e,t){return`You must submit a result matching the required schema before you can stop: call the \`submit_result\` tool with \`request_id: "${t}"\` and the JSON in \`result\`, or with \`decline: { reason, code }\` when the schema cannot be honestly satisfied. You cannot finish or go dormant any other way while this request is pending.
1
+ var v=Object.defineProperty;var o=(e,t)=>v(e,"name",{value:t,configurable:!0});const T="crtr-context",E="crtr-situational-context",R="crtr-context-nudge",O="crtr-review-boundary",Y="You've stopped but you're not waiting on anyone and haven't finished. Pipe the result to `crtr push final` through a single-quoted heredoc if the work is done, or use `crtr human send` if you are blocked or need the user.",I="This conversation has been idle with nothing left to wake it, so it is being concluded. This is your last turn. Use it to leave a trustworthy inheritance, not to restart or broaden the work. Do these four things now, then stop.\n\n1. Establish current truth. Check only state that may have changed outside the transcript and matters to resuming\u2014such as the working tree, a remote run, or an external decision. Do not start new work; perform only a quick check needed to avoid recording an unverified claim. If no mandate or work ever began, record that plainly in the inheritance and keep every artifact minimal.\n\n2. Put supporting material in documents you own (`crtr doc write`) only when it is needed to keep the inheritance concise. Rewrite existing living documents rather than leave superseded versions. Task state, identifiers, and recovery detail belong here, under this node, not under a longer-lived owner.\n\n3. Give a longer-lived owner (`--owner repo|profile|user`) only to a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr doc write -h`, search before writing (`crtr canvas search`), and choose the narrowest owner that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into such a document.\n\n4. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write for someone who has not seen this conversation: name the work in plain terms, say where it stands, and say what remains or is blocked. Put the current outcome in the first line; include only needed decisions and recovery handles, then stop.",$="Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.",L="The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.",S="Your previous turn stopped on a connection error reaching the model provider. Retrying now: continue from where you left off and retry the work that failed.",M="Your previous turn stopped on a provider fault. Continue from where you left off and retry the work that failed.",D="<crtr-review-approval>",j="</crtr-review-approval>",V="<model-fallback-recovery>",B="</model-fallback-recovery>",g="\n```";function F(e,t){return`You must submit a result matching the required schema before you can stop: call the \`submit_result\` tool with \`request_id: "${t}"\` and the JSON in \`result\`, or with \`decline: { reason, code }\` when the schema cannot be honestly satisfied. You cannot finish or go dormant any other way while this request is pending.
2
2
 
3
3
  Required schema:
4
4
 
5
5
  \`\`\`json
6
- ${e}${g}`}o(F,"formatStructuredOutputReprompt");function q(e,t,n){return n==="credential"?`The previous model (${e}) had no usable provider credential, so you were automatically switched to ${t}. Continue the task from where the failed turn left off.`:`The previous model (${e}) was unavailable (provider 404 not_found), so you were automatically switched to ${t}. Continue the task from where the failed turn left off.`}o(q,"formatModelFallbackRecovery");function A(e){switch(e){case R:return"context-nudge";case T:return"bearings";case O:return"review-boundary";case E:return"situational";default:return}}o(A,"legacyCustomTypeKind");const x=/^[A-Za-z_:][A-Za-z0-9_:.-]*$/,l=/\s+([A-Za-z_:][A-Za-z0-9_:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gy;function k(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}o(k,"escapeXmlAttribute");function w(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;")}o(w,"escapeXmlText");function h(e){return e.replace(/&(?:amp|lt|gt|quot|apos);/g,t=>({"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'"})[t])}o(h,"unescapeXmlAttribute");function f(e){const t={};l.lastIndex=0;let n=0;for(;n<e.length&&!/^\s*$/.test(e.slice(n));){l.lastIndex=n;const i=l.exec(e);if(i===null)return null;const[,r,a,u]=i;if(r===void 0||Object.hasOwn(t,r))return null;t[r]=h(a??u??""),n=l.lastIndex}return t}o(f,"parseAttributes");function p(e){return Object.entries(e).filter(([,t])=>t!==void 0).map(([t,n])=>{if(!x.test(t))throw new Error(`Invalid XML attribute name: ${t}`);return` ${t}="${k(String(n))}"`}).join("")}o(p,"formatAttributes");function C(e){return typeof e.content=="string"?e.content:Array.isArray(e.content)?e.content.filter(t=>typeof t=="object"&&t!==null&&t.type==="text"&&typeof t.text=="string").map(t=>t.text).join(""):""}o(C,"generatedContextText");function y(e,t,n){if(e==="")throw new Error("Runtime card kind is required");if(Object.hasOwn(t,"kind"))throw new Error("Runtime card facts cannot replace kind");return`<runtime${p({kind:e,...t})}>${n}</runtime>`}o(y,"formatCard");function X(e,t,n){return y(e,t,w(n))}o(X,"formatDataCard");function P(e){const t=[],n=/<from\b([^>]*)>([\s\S]*?)<\/from>/g;for(const i of e.matchAll(n)){const r=f(i[1]??"");if(r?.id===void 0)continue;const a=[],u=i[2]??"",c=/<entry\b([^>]*)>([\s\S]*?)<\/entry>/g;for(const d of u.matchAll(c)){const s=f(d[1]??"");if(s?.kind===void 0)continue;const v=s.disposition==="human-answer"||s.disposition==="human-canceled"?s.disposition:"report";a.push({kind:s.kind,disposition:v,body:h(d[2]??""),...s.ref===void 0?{}:{ref:s.ref}})}const m=r.updates===void 0?Number.NaN:Number(r.updates);t.push({id:r.id,updates:Number.isFinite(m)?m:a.length,finished:r.finished==="true"||a.some(d=>d.kind==="final"),entries:a})}return t}o(P,"parseInboxBody");function N(e,t,n,i){return{kind:e,facts:t,body:n,senders:i}}o(N,"cardFor");function b(e){const t=/^<runtime\b([^>]*)>/.exec(e);if(t===null||!e.endsWith("</runtime>"))return null;const n=f(t[1]??"");if(n?.kind===void 0)return null;const i=e.slice(t[0].length,-10),{kind:r,...a}=n,u=Object.freeze(a),c=r.includes(":")?h(i):i;return N(r,u,c,r==="inbox"?P(c):[])}o(b,"parseEnvelope");function W(e){const t=C(e),n=b(t);if(n!==null)return n;if(e.role!=="custom")return null;const i=A(e.customType);return i===void 0?null:{kind:i,facts:Object.freeze({}),body:t,senders:[]}}o(W,"parseCard");function z(e){return e.disposition==="human-answer"&&b(e.body.trim())===null}o(z,"isHumanWordsEntry");function K(e){return e.startsWith("/")?`Preview only. Read the full body from file \`${e}\`.`:`Read this report with \`crtr canvas read ${e}\`.`}o(K,"formatInboxRefInstruction");function Z(e){const t=e.map(n=>{const i=n.entries.map(r=>{const a=r.disposition==="human-answer"||r.disposition==="human-canceled"?r.disposition:void 0;return`<entry${p({kind:r.kind,ref:r.ref,disposition:a})}>${w(r.body)}</entry>`}).join(`
7
- `);return`<from${p({id:n.id})}>${i}</from>`}).join(`
8
- `);return y("inbox",{},t)}o(Z,"formatInboxCard");export{$ as AUTH_FAULT_RECOVERY_BODY,L as CONNECTION_FAULT_RECOVERY_BODY,T as CONTEXT_INTRO_CUSTOM_TYPE,R as CONTEXT_NUDGE_CUSTOM_TYPE,I as PARK_SUMMARY_PROMPT,M as PROVIDER_FAULT_RECOVERY_BODY,O as REVIEW_BOUNDARY_CUSTOM_TYPE,S as RUN_CONNECTION_FAULT_RETRY_BODY,E as SITUATIONAL_CONTEXT_CUSTOM_TYPE,Y as STALL_REPROMPT,y as formatCard,X as formatDataCard,Z as formatInboxCard,K as formatInboxRefInstruction,q as formatModelFallbackRecovery,F as formatStructuredOutputReprompt,C as generatedContextText,z as isHumanWordsEntry,W as parseCard};
6
+ ${e}${g}`}o(F,"formatStructuredOutputReprompt");function q(e,t,n){return n==="credential"?`The previous model (${e}) had no usable provider credential, so you were automatically switched to ${t}. Continue the task from where the failed turn left off.`:`The previous model (${e}) was unavailable (provider 404 not_found), so you were automatically switched to ${t}. Continue the task from where the failed turn left off.`}o(q,"formatModelFallbackRecovery");function x(e){switch(e){case R:return"context-nudge";case T:return"bearings";case O:return"review-boundary";case E:return"situational";default:return}}o(x,"legacyCustomTypeKind");const A=/^[A-Za-z_:][A-Za-z0-9_:.-]*$/,l=/\s+([A-Za-z_:][A-Za-z0-9_:.-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/gy;function k(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}o(k,"escapeXmlAttribute");function w(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;")}o(w,"escapeXmlText");function h(e){return e.replace(/&(?:amp|lt|gt|quot|apos);/g,t=>({"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'"})[t])}o(h,"unescapeXmlAttribute");function f(e){const t={};l.lastIndex=0;let n=0;for(;n<e.length&&!/^\s*$/.test(e.slice(n));){l.lastIndex=n;const i=l.exec(e);if(i===null)return null;const[,r,a,u]=i;if(r===void 0||Object.hasOwn(t,r))return null;t[r]=h(a??u??""),n=l.lastIndex}return t}o(f,"parseAttributes");function p(e){return Object.entries(e).filter(([,t])=>t!==void 0).map(([t,n])=>{if(!A.test(t))throw new Error(`Invalid XML attribute name: ${t}`);return` ${t}="${k(String(n))}"`}).join("")}o(p,"formatAttributes");function C(e){return typeof e.content=="string"?e.content:Array.isArray(e.content)?e.content.filter(t=>typeof t=="object"&&t!==null&&t.type==="text"&&typeof t.text=="string").map(t=>t.text).join(""):""}o(C,"generatedContextText");function y(e,t,n){if(e==="")throw new Error("Runtime card kind is required");if(Object.hasOwn(t,"kind"))throw new Error("Runtime card facts cannot replace kind");return`<runtime${p({kind:e,...t})}>${n}</runtime>`}o(y,"formatCard");function X(e,t,n){return y(e,t,w(n))}o(X,"formatDataCard");function P(e){const t=[],n=/<from\b([^>]*)>([\s\S]*?)<\/from>/g;for(const i of e.matchAll(n)){const r=f(i[1]??"");if(r?.id===void 0)continue;const a=[],u=i[2]??"",c=/<entry\b([^>]*)>([\s\S]*?)<\/entry>/g;for(const d of u.matchAll(c)){const s=f(d[1]??"");if(s?.kind===void 0)continue;const _=s.disposition==="human-answer"||s.disposition==="human-canceled"?s.disposition:"report";a.push({kind:s.kind,disposition:_,body:h(d[2]??""),...s.ref===void 0?{}:{ref:s.ref}})}const m=r.updates===void 0?Number.NaN:Number(r.updates);t.push({id:r.id,updates:Number.isFinite(m)?m:a.length,finished:r.finished==="true"||a.some(d=>d.kind==="final"),entries:a})}return t}o(P,"parseInboxBody");function N(e,t,n,i){return{kind:e,facts:t,body:n,senders:i}}o(N,"cardFor");function b(e){const t=/^<runtime\b([^>]*)>/.exec(e);if(t===null||!e.endsWith("</runtime>"))return null;const n=f(t[1]??"");if(n?.kind===void 0)return null;const i=e.slice(t[0].length,-10),{kind:r,...a}=n,u=Object.freeze(a),c=r.includes(":")?h(i):i;return N(r,u,c,r==="inbox"?P(c):[])}o(b,"parseEnvelope");function W(e){const t=C(e),n=b(t);if(n!==null)return n;if(e.role!=="custom")return null;const i=x(e.customType);return i===void 0?null:{kind:i,facts:Object.freeze({}),body:t,senders:[]}}o(W,"parseCard");function z(e){return e.disposition==="human-answer"&&b(e.body.trim())===null}o(z,"isHumanWordsEntry");function K(e){return e.startsWith("/")?`Preview only. Read the full body from file \`${e}\`.`:`Read this report with \`crtr canvas read ${e}\`.`}o(K,"formatInboxRefInstruction");function Z(e){const t=e.map(n=>n.data?.from_machine).find(n=>typeof n=="string");return t===void 0?{}:{machine:t}}o(Z,"machineOfEntries");function G(e){const t=e.map(n=>{const i=n.entries.map(r=>{const a=r.disposition==="human-answer"||r.disposition==="human-canceled"?r.disposition:void 0;return`<entry${p({kind:r.kind,ref:r.ref,disposition:a})}>${w(r.body)}</entry>`}).join(`
7
+ `);return`<from${p({id:n.id,machine:n.machine})}>${i}</from>`}).join(`
8
+ `);return y("inbox",{},t)}o(G,"formatInboxCard");export{$ as AUTH_FAULT_RECOVERY_BODY,L as CONNECTION_FAULT_RECOVERY_BODY,T as CONTEXT_INTRO_CUSTOM_TYPE,R as CONTEXT_NUDGE_CUSTOM_TYPE,I as PARK_SUMMARY_PROMPT,M as PROVIDER_FAULT_RECOVERY_BODY,O as REVIEW_BOUNDARY_CUSTOM_TYPE,S as RUN_CONNECTION_FAULT_RETRY_BODY,E as SITUATIONAL_CONTEXT_CUSTOM_TYPE,Y as STALL_REPROMPT,y as formatCard,X as formatDataCard,G as formatInboxCard,K as formatInboxRefInstruction,q as formatModelFallbackRecovery,F as formatStructuredOutputReprompt,C as generatedContextText,z as isHumanWordsEntry,Z as machineOfEntries,W as parseCard};
@@ -1,10 +1,10 @@
1
- var w=Object.defineProperty;var i=(e,t)=>w(e,"name",{value:t,configurable:!0});import{formatInboxRefInstruction as u}from"./generated-context.js";const f=12,l=1e3,y=1e3;function k(e){return(e===void 0?null:/^([a-z0-9-]+)\/reports\/[^/]+$/.exec(e))?.[1]}i(k,"reportRefNodeId");function x(e){let t=e,r=!1;const o=t.split(`
1
+ var w=Object.defineProperty;var i=(e,t)=>w(e,"name",{value:t,configurable:!0});import{formatInboxRefInstruction as $}from"./generated-context.js";const f=12,l=1e3,y=1e3;function k(e){return(e===void 0?null:/^([a-z0-9-]+)\/reports\/[^/]+$/.exec(e))?.[1]}i(k,"reportRefNodeId");function x(e){let t=e,r=!1;const o=t.split(`
2
2
  `);return o.length>f&&(t=o.slice(0,f).join(`
3
- `),r=!0),t.length>l&&(t=t.slice(0,l),r=!0),{text:t.trimEnd(),clipped:r}}i(x,"clipBody");function j(e){const t=typeof e.data?.body=="string"?String(e.data.body).trim():"";return t===""||t===e.label?"":t}i(j,"inlineBody");function v(e){const t=e.data?.push;return typeof t=="object"&&t!==null&&typeof t.event=="string"?t:null}i(v,"pushOf");function $(e){const t=e.owner_handle??e.owner;return t==null?e.name:`${t}/${e.name}`}i($,"objectRef");function _(e){return e.type==="document"?`[[${$(e)}]]`:e.type==="job"?`job ${e.id} (\`${e.name}\`)`:e.type==="node"?`node ${e.name} (${e.id})`:`${e.type} ${e.name}`}i(_,"objectText");function R(e,t){return t.actor===null?"the runtime":e.from_name===void 0||e.from_name===t.actor?t.actor:`${e.from_name} (${t.actor})`}i(R,"actorText");function m(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}i(m,"list");function A(e,t){const r=_(t.object),o=R(e,t),n=t.attrs,a=t.object.type==="document"?` Read it with \`crtr canvas read ${$(t.object)}\`.`:"";switch(t.event){case"created":return t.object.type==="node"?`${o} created ${r}${typeof n.kind=="string"?`, kind ${n.kind}`:""}.`:`${o} created ${r}.${a}`;case"read":return`${o} read ${r}.`;case"watched":return`${o} started watching ${r}.`;case"deleted":return`${o} deleted ${r}.`;case"edited":{const c=m(n.authors),s=m(n.rationales),d=typeof n.from_revision=="number"&&typeof n.revision=="number"&&n.revision-n.from_revision>1?`revisions ${n.from_revision+1}\u2013${n.revision}`:typeof n.revision=="number"?`revision ${n.revision}`:"a new revision",g=c.length===0?o:c.join(", "),b=s.length===0?"":` Rationale: ${s.join("; ")}.`,p=n.needs_merge===!0?" Two edits were made from the same revision; read it and edit it to merge them.":"",h=` You get this because you read or wrote it; if it no longer concerns your work, stop with \`crtr canvas unwatch ${$(t.object)}\`.`;return`${g} edited ${r} (${d}).${b}${p}${a}${h}`}case"messaged":return`${o} messaged ${r}.`;case"sent":return`${o} sent a message as ${r}.`;case"configured":return`${o} changed the settings of ${r}${typeof n.changed=="string"?`: ${n.changed}`:""}.`;case"exited":return`${r[0].toUpperCase()}${r.slice(1)} exited with code ${String(n["exit-code"])}. Log: ${String(n["log-path"])}`;case"killed":{const c=t.actor===null?"":` by ${o}`,s=typeof n.reason=="string"?` (${n.reason})`:"",d=n.reason==="deadline"?` It was canceled at its deadline; claim it with \`crtr node bash extend ${t.object.id} --for <duration>\` next time.`:"";return`${r[0].toUpperCase()}${r.slice(1)} was killed${c}${s}.${d} Log: ${String(n["log-path"])}`}default:return e.label}}i(A,"renderPush");function E(e,t){if(t!==null&&t.length<y&&e.ref!==void 0)return`${t.trimEnd()}
3
+ `),r=!0),t.length>l&&(t=t.slice(0,l),r=!0),{text:t.trimEnd(),clipped:r}}i(x,"clipBody");function j(e){const t=typeof e.data?.body=="string"?String(e.data.body).trim():"";return t===""||t===e.label?"":t}i(j,"inlineBody");function v(e){const t=e.data?.push;return typeof t=="object"&&t!==null&&typeof t.event=="string"?t:null}i(v,"pushOf");function u(e){const t=e.owner_handle??e.owner;return t==null?e.name:`${t}/${e.name}`}i(u,"objectRef");function _(e){return e.type==="document"?`[[${u(e)}]]`:e.type==="job"?`job ${e.id} (\`${e.name}\`)`:e.type==="node"?`node ${e.name} (${e.id})${e.machine?` on ${e.machine}`:""}`:`${e.type} ${e.name}`}i(_,"objectText");function R(e,t){return t.actor===null?"the runtime":e.from_name===void 0||e.from_name===t.actor?t.actor:`${e.from_name} (${t.actor})`}i(R,"actorText");function m(e){return Array.isArray(e)?e.filter(t=>typeof t=="string"):[]}i(m,"list");function A(e,t){const r=_(t.object),o=R(e,t),n=t.attrs,a=t.object.type==="document"?` Read it with \`crtr canvas read ${u(t.object)}\`.`:"";switch(t.event){case"created":return t.object.type==="node"?`${o} created ${r}${typeof n.kind=="string"?`, kind ${n.kind}`:""}.`:`${o} created ${r}.${a}`;case"read":return`${o} read ${r}.`;case"watched":return`${o} started watching ${r}.`;case"deleted":return`${o} deleted ${r}.`;case"edited":{const c=m(n.authors),s=m(n.rationales),d=typeof n.from_revision=="number"&&typeof n.revision=="number"&&n.revision-n.from_revision>1?`revisions ${n.from_revision+1}\u2013${n.revision}`:typeof n.revision=="number"?`revision ${n.revision}`:"a new revision",g=c.length===0?o:c.join(", "),b=s.length===0?"":` Rationale: ${s.join("; ")}.`,p=n.needs_merge===!0?" Two edits were made from the same revision; read it and edit it to merge them.":"",h=` You get this because you read or wrote it; if it no longer concerns your work, stop with \`crtr canvas unwatch ${u(t.object)}\`.`;return`${g} edited ${r} (${d}).${b}${p}${a}${h}`}case"messaged":return`${o} messaged ${r}.`;case"sent":return`${o} sent a message as ${r}.`;case"configured":return`${o} changed the settings of ${r}${typeof n.changed=="string"?`: ${n.changed}`:""}.`;case"exited":return`${r[0].toUpperCase()}${r.slice(1)} exited with code ${String(n["exit-code"])}. Log: ${String(n["log-path"])}`;case"killed":{const c=t.actor===null?"":` by ${o}`,s=typeof n.reason=="string"?` (${n.reason})`:"",d=n.reason==="deadline"?` It was canceled at its deadline; claim it with \`crtr node bash extend ${t.object.id} --for <duration>\` next time.`:"";return`${r[0].toUpperCase()}${r.slice(1)} was killed${c}${s}.${d} Log: ${String(n["log-path"])}`}default:return e.label}}i(A,"renderPush");function E(e,t){if(t!==null&&t.length<y&&e.ref!==void 0)return`${t.trimEnd()}
4
4
 
5
- ${u(e.ref)}`;const r=j(e);if(r===""){const a=v(e);return a!==null&&e.ref===void 0?A(e,a):e.ref===void 0?e.label:`${e.label}
5
+ ${$(e.ref)}`;const r=j(e);if(r===""){const a=v(e);return a!==null&&e.ref===void 0?A(e,a):e.ref===void 0?e.label:`${e.label}
6
6
 
7
- ${u(e.ref)}`}const{text:o,clipped:n}=x(r);return e.ref!==void 0?`${o}
7
+ ${$(e.ref)}`}const{text:o,clipped:n}=x(r);return e.ref!==void 0?`${o}
8
8
 
9
- ${u(e.ref)}`:n?`${o}
9
+ ${$(e.ref)}`:n?`${o}
10
10
  \u2026 (body clipped)`:o}i(E,"renderEntryBody");export{y as INLINE_REPORT_MAX_CHARS,x as clipBody,j as inlineBody,E as renderEntryBody,k as reportRefNodeId};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/cli",
3
- "version": "0.3.446",
3
+ "version": "0.3.448",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -137,7 +137,7 @@
137
137
  },
138
138
  "license": "UNLICENSED",
139
139
  "dependencies": {
140
- "@north-light/identity": "0.3.446",
140
+ "@north-light/identity": "0.3.448",
141
141
  "@earendil-works/pi-agent-core": "1.1.0",
142
142
  "@earendil-works/pi-ai": "1.1.0",
143
143
  "@earendil-works/pi-coding-agent": "1.1.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/identity",
3
- "version": "0.3.446",
3
+ "version": "0.3.448",
4
4
  "description": "Northlight access-token verification and scope rules",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",