@north-light/crouter 0.3.320 → 0.3.322

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 (119) hide show
  1. package/bin/runtime-selector.mjs +52 -3
  2. package/dist/api/client.d.ts +31 -17
  3. package/dist/api/client.js +2 -2
  4. package/dist/api/dto/broker-ops.d.ts +0 -1
  5. package/dist/api/dto/modelauth.d.ts +15 -0
  6. package/dist/api/dto/node-outcomes.d.ts +6 -0
  7. package/dist/api/dto/nodes.d.ts +4 -3
  8. package/dist/api/dto/review-comments.d.ts +8 -0
  9. package/dist/api/errors.d.ts +6 -3
  10. package/dist/api/errors.js +1 -1
  11. package/dist/api/index.d.ts +2 -2
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/node-transport.d.ts +18 -0
  14. package/dist/api/node-transport.js +1 -0
  15. package/dist/api/routes.d.ts +2 -0
  16. package/dist/api/routes.js +1 -1
  17. package/dist/builtin-memory/crouter-sdk.md +86 -0
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +1 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +1 -1
  20. package/dist/cli.js +8 -2
  21. package/dist/clients/attach/render/chat-view.d.ts +5 -1
  22. package/dist/clients/attach/render/chat-view.js +1 -1
  23. package/dist/clients/attach/session/frames.d.ts +1 -0
  24. package/dist/clients/attach/session/frames.js +1 -1
  25. package/dist/clients/attach/viewer.js +606 -606
  26. package/dist/clients/inbox/review/comments-client.d.ts +8 -1
  27. package/dist/clients/inbox/review/comments-client.js +1 -1
  28. package/dist/clients/inbox/review/companion-pane.d.ts +17 -0
  29. package/dist/clients/inbox/review/companion-pane.js +1 -1
  30. package/dist/clients/inbox/review/document-surface.d.ts +10 -0
  31. package/dist/clients/inbox/review/document-surface.js +2 -2
  32. package/dist/clients/inbox/review/frame.js +1 -1
  33. package/dist/clients/inbox/review/keys.d.ts +2 -0
  34. package/dist/clients/inbox/review/keys.js +1 -1
  35. package/dist/commands/api-client.js +3 -3
  36. package/dist/commands/human/prompts.js +1 -1
  37. package/dist/commands/memory/read.js +2 -2
  38. package/dist/commands/sys/branch.js +1 -1
  39. package/dist/commands/sys/connect.d.ts +1 -0
  40. package/dist/commands/sys/connect.js +3 -0
  41. package/dist/commands/sys/daemon.js +1 -1
  42. package/dist/commands/sys/panels/provider-panel.js +1 -1
  43. package/dist/commands/sys/provider-login.d.ts +8 -0
  44. package/dist/commands/sys/provider-login.js +9 -0
  45. package/dist/commands/sys/setup-core.d.ts +4 -0
  46. package/dist/commands/sys/setup-core.js +4 -4
  47. package/dist/commands/sys.js +1 -1
  48. package/dist/core/canvas/browse/app.js +3 -3
  49. package/dist/core/canvas/canvas.d.ts +3 -3
  50. package/dist/core/canvas/canvas.js +17 -14
  51. package/dist/core/canvas/migrations.js +34 -27
  52. package/dist/core/canvas/node-sql-migration.d.ts +1 -0
  53. package/dist/core/canvas/node-sql-migration.js +28 -2
  54. package/dist/core/canvas/paths.d.ts +8 -4
  55. package/dist/core/canvas/paths.js +1 -1
  56. package/dist/core/canvas/recovery.d.ts +1 -0
  57. package/dist/core/canvas/recovery.js +1 -1
  58. package/dist/core/canvas/worktrees.d.ts +1 -0
  59. package/dist/core/canvas/worktrees.js +1 -1
  60. package/dist/core/config.js +1 -1
  61. package/dist/core/human/requests.d.ts +1 -0
  62. package/dist/core/human/requests.js +17 -13
  63. package/dist/core/review/fork.d.ts +8 -0
  64. package/dist/core/review/fork.js +5 -0
  65. package/dist/core/runtime/broker/daemon-ops.js +1 -1
  66. package/dist/core/runtime/broker/extension-abort.d.ts +3 -0
  67. package/dist/core/runtime/broker/extension-abort.js +1 -1
  68. package/dist/core/runtime/broker.js +1 -1
  69. package/dist/core/runtime/spawn.d.ts +5 -1
  70. package/dist/core/runtime/spawn.js +2 -2
  71. package/dist/core/runtime/stop-guard.d.ts +1 -3
  72. package/dist/core/runtime/stop-guard.js +2 -2
  73. package/dist/core/runtime/stop-signals.d.ts +0 -1
  74. package/dist/core/runtime/stop-signals.js +1 -1
  75. package/dist/core/secrets.d.ts +3 -0
  76. package/dist/core/secrets.js +2 -2
  77. package/dist/core/subscription-state.d.ts +1 -1
  78. package/dist/core/subscription-state.js +3 -3
  79. package/dist/core/termrender/termrender.d.ts +3 -1
  80. package/dist/core/termrender/termrender.js +13 -13
  81. package/dist/core/user-settings.d.ts +4 -0
  82. package/dist/core/user-settings.js +1 -1
  83. package/dist/daemon/api/handlers/broker-ops.js +1 -1
  84. package/dist/daemon/api/handlers/health.d.ts +1 -1
  85. package/dist/daemon/api/handlers/health.js +14 -1
  86. package/dist/daemon/api/handlers/inbox.js +1 -1
  87. package/dist/daemon/api/handlers/modelauth.js +1 -1
  88. package/dist/daemon/api/handlers/review-comments.js +1 -1
  89. package/dist/daemon/api/map.d.ts +4 -2
  90. package/dist/daemon/api/map.js +2 -2
  91. package/dist/daemon/api/server.d.ts +5 -1
  92. package/dist/daemon/api/server.js +4 -4
  93. package/dist/daemon/companion-retire.d.ts +3 -1
  94. package/dist/daemon/companion-retire.js +1 -1
  95. package/dist/daemon/crtrd-cli.js +1 -1
  96. package/dist/daemon/crtrd.js +7 -7
  97. package/dist/daemon/human/finish.d.ts +4 -1
  98. package/dist/daemon/human/finish.js +7 -7
  99. package/dist/daemon/human/settle.d.ts +8 -6
  100. package/dist/daemon/human/settle.js +1 -1
  101. package/dist/daemon/manage.d.ts +1 -0
  102. package/dist/daemon/manage.js +2 -2
  103. package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
  104. package/dist/pi-extensions/canvas-inbox-watcher.js +1 -1
  105. package/dist/pi-extensions/canvas-stophook.js +1 -1
  106. package/dist/types.d.ts +7 -0
  107. package/dist/types.js +1 -1
  108. package/docs/sdk/README.md +54 -0
  109. package/docs/sdk/client.md +92 -0
  110. package/docs/sdk/docker.md +63 -0
  111. package/docs/sdk/errors.md +85 -0
  112. package/docs/sdk/getting-started.md +160 -0
  113. package/docs/sdk/memory.md +107 -0
  114. package/docs/sdk/migration.md +108 -0
  115. package/docs/sdk/nodes.md +193 -0
  116. package/docs/sdk/resources.md +71 -0
  117. package/docs/sdk/streaming.md +124 -0
  118. package/package.json +1 -1
  119. package/runtime.lock.json +6 -6
@@ -1 +1 @@
1
- var p=Object.defineProperty;var f=(s,i)=>p(s,"name",{value:i,configurable:!0});import{clearFrozen as u,endLiveExecution as d,getNode as c}from"../../../core/canvas/index.js";import{isSafeNodeId as m}from"../../../core/canvas/paths.js";import{recordedPidLiveness as l}from"../../../core/canvas/pid.js";import{emitEvent as h}from"../../../core/events/emit.js";import{operationIdContext as _}from"../../../core/events/operation-id.js";import{readInterruptedTurn as y}from"../../../core/canvas/recovery.js";import{isProfilePaused as o}from"../../../core/profiles/manifest.js";import{classifyDeadNode as P}from"../../fleet.js";import{applyParkEvent as k,hasPendingWake as v,parkEvent as z}from"../live-obligation.js";import{FreezeLaneReconciler as L}from"./freeze-lane.js";import{isInBackoff as E}from"./respawn-policy.js";import{handleTerminatingRow as R}from"./terminating.js";import{wantsExecution as I}from"./wants-execution.js";function M(s){return s.status==="done"||s.status==="canceled"||s.status==="dead"}f(M,"isTerminal");class H{static{f(this,"NodeLifecycleReconciler")}freezeLane=new L;async run(i,e){const n=[];for(const r of e.rows)await _.fresh(async()=>{try{await this.classifyRow(i,r,e,n)}catch(t){const a=r.node_id;h({level:"error",event:"node.lifecycle.classify_failed",...m(a)?{node_id:a}:{fields:{affected_node_id:a}},error:t})}});return this.freezeLane.run(i,{fleet:e.fleet,candidates:n})}async classifyRow(i,e,n,r){const t=e.node_id;if(M(e)){if(R(e,n.fleet.has(t)))return;e.pi_pid!=null&&l(e.pi_pid,e.pi_pid_identity)==="dead"&&d(t,{reason:"observed-dead"}),e.frozen_at!=null&&r.push({row:e,enact:"thaw"});return}if(n.fleet.has(t)){o(e.profile_id)?n.fleet.interruptForProfilePause(t):this.thawMark(e);return}if(o(e.profile_id)){this.freezeLane.endEpisode(t);return}if(!I(e)){this.thawMark(e),this.settleParked(e);return}if(E(e,i))return;if(e.status==="idle"){r.push({row:e,enact:"idle-release"});return}if(e.pi_pid==null){if(e.respawn_not_before!=null){r.push({row:e,enact:"policy"});return}e.frozen_at!=null&&r.push({row:e,enact:"thaw"});return}if(l(e.pi_pid,e.pi_pid_identity)!=="dead")return;n.fleet.hasProfilePauseInterruption(t)||d(t,{reason:"observed-dead"});const a=P(c(t),y(t)?.interrupted===!0,v(t));if(a==="respawn-fresh"||a==="respawn-cycle"||a==="respawn-resume"){r.push({row:e,enact:"policy"});return}await n.fleet.applyDeadRowPolicy(t),this.thawMark(e)}thawMark(i){i.frozen_at!=null&&u(i.node_id)}settleParked(i){if(i.status!=="idle")return;this.freezeLane.endEpisode(i.node_id);const e=z(i.node_id,i.lifecycle);e!=="release"&&k(i.node_id,e)}}export{H as NodeLifecycleReconciler};
1
+ var u=Object.defineProperty;var f=(a,i)=>u(a,"name",{value:i,configurable:!0});import{clearFrozen as c,endLiveExecution as l,getNode as m}from"../../../core/canvas/index.js";import{isSafeNodeId as h}from"../../../core/canvas/paths.js";import{recordedPidLiveness as o}from"../../../core/canvas/pid.js";import{emitEvent as _}from"../../../core/events/emit.js";import{operationIdContext as y}from"../../../core/events/operation-id.js";import{readInterruptedTurn as P}from"../../../core/canvas/recovery.js";import{isProfilePaused as p}from"../../../core/profiles/manifest.js";import{classifyDeadNode as k}from"../../fleet.js";import{applyParkEvent as v,hasPendingWake as z,parkEvent as L}from"../live-obligation.js";import{FreezeLaneReconciler as E}from"./freeze-lane.js";import{isInBackoff as I}from"./respawn-policy.js";import{handleTerminatingRow as R}from"./terminating.js";import{wantsExecution as M}from"./wants-execution.js";function N(a){return a.status==="done"||a.status==="canceled"||a.status==="dead"}f(N,"isTerminal");class J{static{f(this,"NodeLifecycleReconciler")}freezeLane=new E;async run(i,e){const n=[];for(const r of e.rows)await y.fresh(async()=>{try{await this.classifyRow(i,r,e,n)}catch(t){const s=r.node_id;_({level:"error",event:"node.lifecycle.classify_failed",...h(s)?{node_id:s}:{fields:{affected_node_id:s}},error:t})}});return this.freezeLane.run(i,{fleet:e.fleet,candidates:n})}async classifyRow(i,e,n,r){const t=e.node_id;if(N(e)){if(R(e,n.fleet.has(t)))return;e.pi_pid!=null&&o(e.pi_pid,e.pi_pid_identity)==="dead"&&l(t,{reason:"observed-dead"}),e.frozen_at!=null&&r.push({row:e,enact:"thaw"});return}if(n.fleet.has(t)){p(e.profile_id)?n.fleet.interruptForProfilePause(t):this.thawMark(e);return}if(p(e.profile_id)){this.freezeLane.endEpisode(t);return}const s=e.pi_pid!=null&&o(e.pi_pid,e.pi_pid_identity)==="dead";if(s&&!n.fleet.hasProfilePauseInterruption(t)&&l(t,{reason:"observed-dead"}),!M(e)){this.thawMark(e),this.settleParked(e);return}if(I(e,i))return;if(e.status==="idle"){r.push({row:e,enact:"idle-release"});return}if(e.pi_pid==null){if(e.respawn_not_before!=null){r.push({row:e,enact:"policy"});return}e.frozen_at!=null&&r.push({row:e,enact:"thaw"});return}if(!s)return;const d=k(m(t),P(t)?.interrupted===!0,z(t));if(d==="respawn-fresh"||d==="respawn-cycle"||d==="respawn-resume"){r.push({row:e,enact:"policy"});return}await n.fleet.applyDeadRowPolicy(t),this.thawMark(e)}thawMark(i){i.frozen_at!=null&&c(i.node_id)}settleParked(i){if(i.status!=="idle")return;this.freezeLane.endEpisode(i.node_id);const e=L(i.node_id,i.lifecycle);e!=="release"&&v(i.node_id,e)}}export{J as NodeLifecycleReconciler};
@@ -1 +1 @@
1
- var N=Object.defineProperty;var f=(s,t)=>N(s,"name",{value:t,configurable:!0});import{envNodeId as S,envProfileId as A,envWatcherDebounceMs as O,envWatcherTickMs as P}from"../shared/env.js";import{coalesceBrokerInbox as H,entryCards as T,reportRefNodeId as D}from"../core/runtime/broker/inbox.js";import{acknowledgeBrokerMail as U,claimBrokerMail as q,brokerExtensionState as B}from"../core/runtime/broker/daemon-ops.js";import{wasTurnRefused as J}from"../core/runtime/broker/extension-abort.js";import{announceMessageOrigin as $}from"../core/runtime/stamp/channel.js";import{captureGoalIfAbsent as W,generateAndCommitInitialName as G}from"./broker-local.js";import{emitEvent as X}from"../core/events/emit.js";import{errorClassFromError as L}from"../core/events/errors.js";import{operationIdContext as u}from"../core/events/operation-id.js";import{situationalContextEnvelope as V,SITUATIONAL_CONTEXT_CUSTOM_TYPE as w}from"../core/runtime/situational-context.js";import{isProfilePaused as j}from"../core/profiles/manifest.js";import{brokerIdleTurnStart as z,HELD_DEFERRED_INBOX_PROMPT_JOIN as Y}from"../core/runtime/broker/held-deferred-inbox.js";const g=64,C=16;let p,K=0;class x extends Error{static{f(this,"WatcherFatalError")}constructor(t,e){super(t,{cause:e}),this.name="WatcherFatalError"}}function Q(){return{tickMs:P(),debounceMs:O()}}f(Q,"resolveCadence");function m(){const s=S();return s===void 0||s.trim()===""?void 0:s}f(m,"currentNodeId");function k(){const s=A();return s!==void 0&&s!==""&&j(s)}f(k,"profilePaused");function F(s){try{return L(s)}catch{return{class:"unknown"}}}f(F,"safeErrorClass");function v(s){try{X(s)}catch{}}f(v,"emitBestEffort");function M(s){try{u.fresh(t=>{v({...s,operation_id:t})})}catch{}}f(M,"emitFreshBestEffort");function b(s,t){try{u.run(s,()=>{v({...t,operation_id:s})})}catch{}}f(b,"emitStoredBestEffort");function Z(s){return s.kind==="human"?{type:"human"}:{type:"runtime",kind:"inbox",entries:s.entries.map(t=>({...t.from===null?{}:{from:t.from},...t.from_name===void 0?{}:{fromName:t.from_name},kind:t.kind,tier:t.tier,...t.ref===void 0?{}:{ref:t.ref}}))}}f(Z,"deliveryOrigin");function _(s,t){if(s.length===0)throw new Error(`cannot create an empty ${t} delivery`);const e=[...new Set(s.map(n=>n.operation_id))].sort(),i=e.length-C;return{channel:t,entryIds:s.map(n=>n.entry_id),throughEntryId:s.at(-1).entry_id,sourceOperationIds:e.slice(0,C),...i>0?{sourceOperationIdsOmitted:i}:{}}}f(_,"deliveryFacts");function y(s){return{channel:s.channel,entry_ids:s.entryIds,through_entry_id:s.throughEntryId,source_operation_ids:s.sourceOperationIds,...s.sourceOperationIdsOmitted===void 0?{}:{source_operation_ids_omitted:s.sourceOperationIdsOmitted}}}f(y,"deliveryFields");function ee(s){b(s.operationId,{level:"info",event:"inbox.delivery.completed",outcome:"succeeded",fields:y(s)})}f(ee,"emitDeliveryCompleted");function I(s,t,e,i){b(s,{level:"error",event:"inbox.delivery.failed",outcome:"failed",error:i,error_class:F(i),fields:{stage:e,...y(t)}})}f(I,"emitDeliveryFailure");function E(s,t){b(s.operationId,{level:"error",event:"inbox.delivery.failed",outcome:"failed",error_class:{class:"unknown"},fields:{stage:"abandoned",reason:t,...y(s)}})}f(E,"emitDeliveryAbandoned");function R(s){return s==="quit"||s==="reload"||s==="new"||s==="resume"||s==="fork"}f(R,"isSessionShutdownReason");function te(s){switch(s){case"quit":return"shutdown";case"reload":case"new":case"resume":case"fork":return"reload";default:return s}}f(te,"disposalReasonForShutdown");function ie(s){return typeof s=="string"?s.slice(0,128):s===null?"null":s===void 0?"undefined":typeof s=="number"||typeof s=="boolean"||typeof s=="bigint"?String(s).slice(0,128):typeof s}f(ie,"boundedRuntimeReason");class re{static{f(this,"CanvasInboxWatcher")}pi;cadence;generation;timer;armed=!1;lastCtx;streaming=!1;nodeId;claims=new Map;tickInFlight;buffer=[];lastArrival=0;pendingHandoffs=[];pendingCommit;promptJoinInFlight=!1;seeded=!1;disposed=!1;disposalReason;fatalError;fatalEscaped=!1;reportNodes=new Map;reportNodesAsked=new Set;reportNodesRefreshing=!1;constructor(t,e,i){this.pi=t,this.cadence=e,this.generation=i}start(){const t=f((e,i)=>{this.canMutate()&&this.captureCtx(e,i)},"capture");this.pi.on("session_start",(e,i)=>{this.canMutate()&&(this.captureCtx(e,i),this.arm(),this.refreshReportNodes(m()))}),this.pi.on("turn_end",t),this.pi.on("agent_start",(e,i)=>{this.canMutate()&&(this.captureCtx(e,i),this.streaming=!0)}),this.pi.on("agent_end",(e,i)=>{this.canMutate()&&(this.captureCtx(e,i),this.streaming=!1)}),this.pi.on("agent_settled",async(e,i)=>{if(!this.canMutate())return;this.captureCtx(e,i);const n=m();if(n!==void 0)try{this.assertNodeOwnership(n,"settle"),await this.tickInFlight,this.seeded||await this.claimMail(n),await this.settle(n,i,!1,!0)==="refresh_held"&&this.dispose("refresh",!1)}catch(r){this.dispose("shutdown",!1,void 0,{error:r,stage:"settle"})}}),this.pi.on("session_shutdown",async e=>{if(!this.canMutate())return;let i;try{i=e?.reason}catch{i=void 0}const n=R(i)?te(i):"reload";R(i)||M({level:"warn",event:"inbox.lifecycle.unknown_shutdown_reason",outcome:"failed",fields:{reason:ie(i)}});try{await this.disposeAtSettlement(n,this.lastCtx)}catch(r){this.dispose(n,!1,void 0,{error:r,stage:"settle"})}})}async refreshReportNodes(t){if(t!==void 0){this.reportNodesRefreshing=!0;try{const e=await B(t);if(!this.canMutate())return;this.reportNodes=new Map(e.report_nodes.map(i=>[i.node_id,i]))}catch{}finally{this.reportNodesRefreshing=!1}}}refreshForUnknownReportRefs(){const t=[];for(const e of this.buffer){const i=D(e.ref);i!==void 0&&(this.reportNodes.has(i)||this.reportNodesAsked.has(i)||t.push(i))}if(t.length===0)return!1;for(const e of t)this.reportNodesAsked.add(e);return this.refreshReportNodes(m()),this.reportNodesRefreshing}arm(){this.armed||(this.armed=!0,this.timer=setInterval(()=>this.runTick(),this.cadence.tickMs),typeof this.timer.unref=="function"&&this.timer.unref(),this.runTick("schedule"))}async disposeAtSettlement(t,e){const i=m();if(i===void 0){this.dispose(t,!1);return}try{this.assertNodeOwnership(i,"settle");const n=await this.settle(i,e,t!=="refresh");this.dispose(n==="refresh_held"?"refresh":t,!1)}catch(n){this.dispose(t,!1,void 0,{error:n,stage:"settle"})}}dispose(t,e,i,n){if(this.disposed)return;this.disposed=!0,this.disposalReason=t,this.clearTimer();const r=n;{const o=[...this.pendingCommit?.handoffs??[],...this.pendingHandoffs.splice(0)];this.buffer.length=0,this.claims.clear(),this.reportNodes.clear(),this.reportNodesAsked.clear(),this.pendingCommit=void 0,this.promptJoinInFlight=!1,this.nodeId=void 0,this.lastCtx=void 0,this.lastArrival=0,this.streaming=!1,this.seeded=!1,this.ownsCurrentGeneration()&&(p=void 0);for(const a of o)E(a,t)}if(r!==void 0){this.scheduleHandlerFatal(r.error,r.stage);return}}ownsCurrentGeneration(){return p===this&&p.generation===this.generation}canMutate(){return this.ownsCurrentGeneration()&&!this.disposed&&this.fatalError===void 0}captureCtx(t,e){e!==void 0&&(this.lastCtx=e)}clearTimer(){this.timer!==void 0&&(clearInterval(this.timer),this.timer=void 0)}isIdle(){try{if(typeof this.lastCtx?.isIdle=="function")return this.lastCtx.isIdle()===!0}catch{}return!this.streaming}assertNodeOwnership(t,e){!this.seeded||this.nodeId===void 0||this.nodeId===t||this.raiseReadFatal(e,new Error(`watcher node changed from ${this.nodeId} to ${t}`))}raiseFatal(t,e,i,n){throw this.fatalError===void 0&&(this.fatalError=new x(e,i),this.clearTimer(),M({level:"error",event:t,outcome:"failed",error:i,error_class:F(i),fields:n})),this.fatalError}raiseReadFatal(t,e){return this.raiseFatal("inbox.read.failed",`inbox ${t} state is corrupt`,e,{stage:t})}raiseSituationalFatal(t,e){return this.raiseFatal("inbox.situational_context.missing","situational context marker has no readable sidecar",e,{entry_id:t})}escapeFatal(t,e){if(!this.fatalEscaped){if(this.fatalEscaped=!0,e==="throw")throw t;queueMicrotask(()=>{throw t})}}scheduleHandlerFatal(t,e){let i;if(t instanceof x)i=t;else try{this.raiseReadFatal(e,t)}catch(n){i=n}this.escapeFatal(i,"schedule")}runTick(t="throw"){if(!this.canMutate()||this.tickInFlight!==void 0)return;const e=this.tick().catch(i=>{this.canMutate()&&this.scheduleHandlerFatal(i,"poll")}).finally(()=>{this.tickInFlight===e&&(this.tickInFlight=void 0)});this.tickInFlight=e}async claimMail(t){this.assertNodeOwnership(t,"poll");const e=await q(t,"wake");if(!this.canMutate()||e.action==="hold")return!1;this.nodeId=t,this.seeded=!0;const i=new Set(e.deliveries.map(r=>r.entry.entry_id)),n=this.buffer.filter(r=>!i.has(r.entry_id));for(const r of n)this.claims.delete(r.entry_id);this.buffer.splice(0,this.buffer.length,...this.buffer.filter(r=>i.has(r.entry_id)));for(const r of e.deliveries){const o=r.entry.entry_id;this.claims.has(o)||(this.claims.set(o,r),r.handoff==="inherited"?u.fresh(a=>{this.pendingHandoffs.push({..._([r.entry],"visible"),operationId:a,settleAtAgentSettled:!0})}):(this.buffer.push(r.entry),this.lastArrival=Date.now()))}return!0}async tick(){const t=m();if(t!==void 0){if(this.pendingCommit!==void 0){await this.retryPendingCommit(t)==="refresh_held"&&this.canMutate()&&this.dispose("refresh",!1);return}if(!(this.promptJoinInFlight||k())&&!(this.pendingHandoffs.length>=g)&&await this.claimMail(t)&&this.buffer.length>0&&Date.now()-this.lastArrival>=this.cadence.debounceMs){if(this.refreshForUnknownReportRefs())return;this.flushOrdinary(t)}}}missingChannelEntries(t){const e=t.filter(r=>this.claims.has(r.entry_id)),i=this.coveredEntryIds("hidden"),n=this.coveredEntryIds("visible");return{hiddenEntries:e.filter(r=>(r.data?.situational===!0||r.data?.situationalOnly===!0)&&!i.has(r.entry_id)),visibleEntries:e.filter(r=>r.data?.situationalOnly!==!0&&!n.has(r.entry_id))}}visibleRoute(t){return t.length===0?"none":this.isIdle()?"idle":t.some(e=>e.tier==="critical")?"critical":t.some(e=>e.tier==="urgent"||e.kind==="final")?"steer":"followUp"}buildPlan(t){const{hiddenEntries:e,visibleEntries:i}=this.missingChannelEntries(t),n=new Set(e.map(c=>c.entry_id)),r=new Set(i.map(c=>c.entry_id)),o=t.filter(c=>n.has(c.entry_id)||r.has(c.entry_id)),a=this.isIdle()&&o.length>0&&o.every(c=>c.tier==="deferred"),d=this.visibleRoute(i)==="critical",h=!a&&!d&&e.length>0,l=!a&&!d&&i.length>0;return{hiddenEntries:e,visibleEntries:i,deferredHold:a,sendCount:Number(h)+Number(l)}}ordinaryPlan(){return this.buildPlan(this.buffer)}contextCards(t,e){const i=[];if(e.some(n=>n.data?.situational===!0)){const n=e.at(-1).entry_id;let r;try{r=V(t)}catch(o){this.raiseSituationalFatal(n,o)}r===null&&this.raiseSituationalFatal(n,new Error(`missing situational context for entry ${n}`)),i.push(r)}for(const n of e)n.data?.situationalOnly===!0&&i.push(...T(n));return i}digest(t,e){try{return H(t,this.reportNodes)}catch(i){this.raiseReadFatal(e,i)}}sendDelivery(t,e,i,n){this.pendingHandoffs.length>=g&&this.raiseReadFatal(n,new Error(`pending handoff capacity exceeded before ${t} send`));let r;try{r=_(e,t)}catch(d){this.raiseReadFatal(n,d)}let o;try{u.fresh(d=>{o=d,v({level:"info",event:"inbox.delivery.started",operation_id:d,fields:y(r)}),i()})}catch(d){return o===void 0&&this.raiseReadFatal(n,d),I(o,r,"send",d),"failed"}o===void 0&&this.raiseReadFatal(n,new Error(`${t} delivery returned without an operation id`));const a={operationId:o,...r};return this.canMutate()?(this.pendingHandoffs.push(a),"handed-off"):(this.disposed&&this.disposalReason!==void 0&&E(a,this.disposalReason),"inactive")}sendContext(t,e,i,n,r,o){return this.sendDelivery("hidden",e,()=>{const a=n==="steer"||n==="followUp"?n:this.isIdle()?void 0:"followUp";i.forEach((d,h)=>{const l=r&&h===i.length-1;this.pi.sendMessage({customType:w,content:d,display:!0,details:{nodeId:t}},a!==void 0?{deliverAs:a}:l?{deliverAs:"followUp",triggerTurn:!0}:{})})},o)}sendVisible(t,e,i,n,r,o){return this.sendDelivery("visible",t,()=>{if(r!=="idle"&&r!=="steer"&&r!=="followUp")throw new Error(`visible send requested for non-send route ${r}`);for(const a of e)this.pi.sendMessage({customType:w,content:a,display:!0},r==="idle"?{}:{deliverAs:r});if($(i,n),r==="idle"){const a=z();if(a===void 0)throw new Error("broker idle-turn admission is unavailable");a(i)}else this.pi.sendUserMessage(i,{deliverAs:r})},o)}abortCritical(){try{this.lastCtx?.abort?.()}catch{}}requeueFromBatch(t,e){if(e.size===0)return;const i=t.filter(o=>e.has(o.entry_id)),n=new Set(i.map(o=>o.entry_id)),r=this.buffer.filter(o=>!n.has(o.entry_id));this.buffer.splice(0,this.buffer.length,...i,...r)}executePlan(t,e,i){const n=new Set,r=this.visibleRoute(e.visibleEntries);if(r==="critical"){if(this.abortCritical(),!this.canMutate())return{inactive:!0,requeueEntryIds:n};for(const h of e.visibleEntries)n.add(h.entry_id);for(const h of e.hiddenEntries)n.add(h.entry_id);return{inactive:!1,requeueEntryIds:n}}const o=e.visibleEntries.length>0?this.digest(e.visibleEntries,i):[],[a,...d]=o;for(const h of d)for(const l of h.entries)n.add(l.entry_id);if(e.hiddenEntries.length>0){const h=this.contextCards(t,e.hiddenEntries),l=this.sendContext(t,e.hiddenEntries,h,r,a===void 0,i);if(l==="inactive")return{inactive:!0,requeueEntryIds:n};if(l==="failed")for(const c of e.hiddenEntries)n.add(c.entry_id)}if(!this.canMutate())return{inactive:!0,requeueEntryIds:n};if(a!==void 0){const h=this.sendVisible(a.entries,a.cards,a.text,Z(a),r,i);if(h==="inactive")return{inactive:!0,requeueEntryIds:n};if(h==="failed")for(const l of a.entries)n.add(l.entry_id);else a.kind==="human"&&(W(t,a.text),G(t,a.text))}return{inactive:!1,requeueEntryIds:n}}flushOrdinary(t){if(this.buffer.length===0)return"empty";const e=this.ordinaryPlan();if(e.deferredHold)return"held";if(this.pendingHandoffs.length+e.sendCount>g)return"capacity-blocked";const i=[...this.buffer],n=this.executePlan(t,e,"poll");return n.inactive||(this.buffer.splice(0,i.length),this.requeueFromBatch(i,n.requeueEntryIds)),"attempted"}async prependHeldDeferredInbox(t){if(!this.canMutate()||this.nodeId===void 0||k())return;const e=this.ordinaryPlan();if(!e.deferredHold)return;const i=e.visibleEntries.length===0?[]:this.digest(e.visibleEntries,"poll"),n=+(e.hiddenEntries.length>0)+i.length;if(this.pendingHandoffs.length+n>g)return;const r=[...this.buffer];this.promptJoinInFlight=!0;try{e.hiddenEntries.length>0&&await this.sendPromptJoinDelivery(t,"hidden",e.hiddenEntries,this.contextCards(this.nodeId,e.hiddenEntries));for(const o of i)await this.sendPromptJoinDelivery(t,"visible",o.entries,[...o.cards,o.text]);if(!this.canMutate())return;this.buffer.splice(0,r.length)}finally{this.promptJoinInFlight=!1}}async sendPromptJoinDelivery(t,e,i,n){let r;try{r=_(i,e)}catch(d){this.raiseReadFatal("poll",d)}let o;try{await u.fresh(async d=>{o=d,v({level:"info",event:"inbox.delivery.started",operation_id:d,fields:y(r)});for(const h of n)await t.sendCustomMessage({customType:w,content:h,display:!0})})}catch(d){throw o===void 0&&this.raiseReadFatal("poll",d),I(o,r,"send",d),d}o===void 0&&this.raiseReadFatal("poll",new Error(`${e} prompt-join delivery returned without an operation id`));const a={operationId:o,...r,settleAtAgentSettled:!0};if(!this.canMutate()){this.disposed&&this.disposalReason!==void 0&&E(a,this.disposalReason);return}this.pendingHandoffs.push(a)}coveredEntryIds(t){const e=new Set;for(const i of this.pendingHandoffs)if(i.channel===t)for(const n of i.entryIds)e.add(n);return e}retryPendingCommit(t){const e=this.pendingCommit;if(e===void 0)return Promise.resolve("settled");if(e.attempt!==void 0)return e.attempt;const i=this.attemptPendingCommit(t,e).finally(()=>{e.attempt===i&&(e.attempt=void 0)});return e.attempt=i,i}async attemptPendingCommit(t,e){let i=!1;try{const r=await U(t,e.receipts);if(r.action==="hold")return"refresh_held";for(const o of e.receipts){const a=r.results.find(d=>d.delivery_id===o.delivery_id);if(a?.outcome!==o.disposition&&a?.outcome!=="stale")throw new Error("daemon returned no terminal mail settlement outcome")}i=r.results.every(o=>o.outcome==="acknowledged")}catch(r){if(this.pendingCommit===e)for(const o of e.handoffs)I(o.operationId,o,"receipt_commit",r);return"held"}if(this.pendingCommit!==e)return"settled";for(const r of e.entryIds)this.claims.delete(r);if(e.receipts.some(r=>r.disposition==="released")){const r=new Set(e.entryIds);this.buffer.splice(0,this.buffer.length,...this.buffer.filter(o=>!r.has(o.entry_id)))}this.pendingCommit=void 0;const n=this.detachPending(r=>r.entryIds.every(o=>!this.claims.has(o)));for(const r of n)i&&ee(r);return"settled"}async settle(t,e,i,n=!1){if(i)try{if(e?.hasPendingMessages?.()===!0)return"held"}catch{}if(this.pendingCommit!==void 0)return this.retryPendingCommit(t);const r=J(),o=new Set(this.buffer.map(l=>l.entry_id)),a=this.pendingHandoffs.filter(l=>n||l.settleAtAgentSettled!==!0),d=new Set(a.flatMap(l=>[...l.entryIds])),h=[...this.claims.keys()].filter(l=>r||d.has(l)&&!o.has(l));return h.length===0?"settled":(this.pendingCommit={handoffs:a,entryIds:h,receipts:h.map(l=>{const c=this.claims.get(l);return{delivery_id:c.delivery_id,attempts:c.attempts,disposition:r?"released":"acknowledged"}})},this.retryPendingCommit(t))}detachPending(t){const e=[];for(let i=this.pendingHandoffs.length-1;i>=0;i-=1){const n=this.pendingHandoffs[i];t(n)&&e.push(...this.pendingHandoffs.splice(i,1))}return e.reverse(),e}}function ne(s,t){p?.dispose("reload",!1);const e=new re(s,t,++K);p=e;const i=f(n=>e.prependHeldDeferredInbox(n),"promptJoin");globalThis[Y]=i;try{e.start()}catch(n){throw e.dispose("reload",!1),n}return e}f(ne,"installWatcher");function se(s){const t=ne(s,Q());return()=>t.dispose("shutdown",!1)}f(se,"registerCanvasInboxWatcher");var we=se;export{we as default,se as registerCanvasInboxWatcher};
1
+ var S=Object.defineProperty;var f=(s,t)=>S(s,"name",{value:t,configurable:!0});import{envNodeId as A,envProfileId as O,envWatcherDebounceMs as P,envWatcherTickMs as H}from"../shared/env.js";import{coalesceBrokerInbox as T,entryCards as D,reportRefNodeId as U}from"../core/runtime/broker/inbox.js";import{acknowledgeBrokerMail as q,claimBrokerMail as B,brokerExtensionState as J}from"../core/runtime/broker/daemon-ops.js";import{wasTurnRefused as $}from"../core/runtime/broker/extension-abort.js";import{isDaemonTransportApiError as k}from"../api/errors.js";import{announceMessageOrigin as W}from"../core/runtime/stamp/channel.js";import{captureGoalIfAbsent as G,generateAndCommitInitialName as X}from"./broker-local.js";import{emitEvent as L}from"../core/events/emit.js";import{errorClassFromError as V}from"../core/events/errors.js";import{operationIdContext as u}from"../core/events/operation-id.js";import{situationalContextEnvelope as j,SITUATIONAL_CONTEXT_CUSTOM_TYPE as w}from"../core/runtime/situational-context.js";import{isProfilePaused as z}from"../core/profiles/manifest.js";import{brokerIdleTurnStart as Y,HELD_DEFERRED_INBOX_PROMPT_JOIN as K}from"../core/runtime/broker/held-deferred-inbox.js";const g=64,F=16;let p,Q=0;class M extends Error{static{f(this,"WatcherFatalError")}constructor(t,e){super(t,{cause:e}),this.name="WatcherFatalError"}}function Z(){return{tickMs:H(),debounceMs:P()}}f(Z,"resolveCadence");function m(){const s=A();return s===void 0||s.trim()===""?void 0:s}f(m,"currentNodeId");function R(){const s=O();return s!==void 0&&s!==""&&z(s)}f(R,"profilePaused");function b(s){try{return V(s)}catch{return{class:"unknown"}}}f(b,"safeErrorClass");function v(s){try{L(s)}catch{}}f(v,"emitBestEffort");function _(s){try{u.fresh(t=>{v({...s,operation_id:t})})}catch{}}f(_,"emitFreshBestEffort");function I(s,t){try{u.run(s,()=>{v({...t,operation_id:s})})}catch{}}f(I,"emitStoredBestEffort");function ee(s){return s.kind==="human"?{type:"human"}:{type:"runtime",kind:"inbox",entries:s.entries.map(t=>({...t.from===null?{}:{from:t.from},...t.from_name===void 0?{}:{fromName:t.from_name},kind:t.kind,tier:t.tier,...t.ref===void 0?{}:{ref:t.ref}}))}}f(ee,"deliveryOrigin");function E(s,t){if(s.length===0)throw new Error(`cannot create an empty ${t} delivery`);const e=[...new Set(s.map(r=>r.operation_id))].sort(),i=e.length-F;return{channel:t,entryIds:s.map(r=>r.entry_id),throughEntryId:s.at(-1).entry_id,sourceOperationIds:e.slice(0,F),...i>0?{sourceOperationIdsOmitted:i}:{}}}f(E,"deliveryFacts");function y(s){return{channel:s.channel,entry_ids:s.entryIds,through_entry_id:s.throughEntryId,source_operation_ids:s.sourceOperationIds,...s.sourceOperationIdsOmitted===void 0?{}:{source_operation_ids_omitted:s.sourceOperationIdsOmitted}}}f(y,"deliveryFields");function te(s){I(s.operationId,{level:"info",event:"inbox.delivery.completed",outcome:"succeeded",fields:y(s)})}f(te,"emitDeliveryCompleted");function C(s,t,e,i){I(s,{level:"error",event:"inbox.delivery.failed",outcome:"failed",error:i,error_class:b(i),fields:{stage:e,...y(t)}})}f(C,"emitDeliveryFailure");function x(s,t){I(s.operationId,{level:"error",event:"inbox.delivery.failed",outcome:"failed",error_class:{class:"unknown"},fields:{stage:"abandoned",reason:t,...y(s)}})}f(x,"emitDeliveryAbandoned");function N(s){return s==="quit"||s==="reload"||s==="new"||s==="resume"||s==="fork"}f(N,"isSessionShutdownReason");function ie(s){switch(s){case"quit":return"shutdown";case"reload":case"new":case"resume":case"fork":return"reload";default:return s}}f(ie,"disposalReasonForShutdown");function re(s){return typeof s=="string"?s.slice(0,128):s===null?"null":s===void 0?"undefined":typeof s=="number"||typeof s=="boolean"||typeof s=="bigint"?String(s).slice(0,128):typeof s}f(re,"boundedRuntimeReason");class ne{static{f(this,"CanvasInboxWatcher")}pi;cadence;generation;timer;armed=!1;lastCtx;streaming=!1;nodeId;claims=new Map;tickInFlight;buffer=[];lastArrival=0;pendingHandoffs=[];pendingCommit;promptJoinInFlight=!1;replayUnseenInherited=!1;seeded=!1;disposed=!1;disposalReason;fatalError;fatalEscaped=!1;reportNodes=new Map;reportNodesAsked=new Set;reportNodesRefreshing=!1;constructor(t,e,i){this.pi=t,this.cadence=e,this.generation=i}start(){const t=f((e,i)=>{this.canMutate()&&this.captureCtx(e,i)},"capture");this.pi.on("session_start",(e,i)=>{this.canMutate()&&(this.captureCtx(e,i),this.arm(),this.refreshReportNodes(m()))}),this.pi.on("turn_end",t),this.pi.on("agent_start",(e,i)=>{this.canMutate()&&(this.captureCtx(e,i),this.streaming=!0)}),this.pi.on("agent_end",(e,i)=>{this.canMutate()&&(this.captureCtx(e,i),this.streaming=!1)}),this.pi.on("agent_settled",async(e,i)=>{if(!this.canMutate())return;this.captureCtx(e,i);const r=m();if(r!==void 0)try{this.assertNodeOwnership(r,"settle"),await this.tickInFlight,this.seeded||await this.claimMail(r),await this.settle(r,i,!1,!0)==="refresh_held"&&this.dispose("refresh",!1)}catch(n){this.dispose("shutdown",!1,void 0,{error:n,stage:"settle"})}}),this.pi.on("session_shutdown",async e=>{if(!this.canMutate())return;let i;try{i=e?.reason}catch{i=void 0}const r=N(i)?ie(i):"reload";N(i)||_({level:"warn",event:"inbox.lifecycle.unknown_shutdown_reason",outcome:"failed",fields:{reason:re(i)}});try{await this.disposeAtSettlement(r,this.lastCtx)}catch(n){this.dispose(r,!1,void 0,{error:n,stage:"settle"})}})}async refreshReportNodes(t){if(t!==void 0){this.reportNodesRefreshing=!0;try{const e=await J(t);if(!this.canMutate())return;this.reportNodes=new Map(e.report_nodes.map(i=>[i.node_id,i]))}catch{}finally{this.reportNodesRefreshing=!1}}}refreshForUnknownReportRefs(){const t=[];for(const e of this.buffer){const i=U(e.ref);i!==void 0&&(this.reportNodes.has(i)||this.reportNodesAsked.has(i)||t.push(i))}if(t.length===0)return!1;for(const e of t)this.reportNodesAsked.add(e);return this.refreshReportNodes(m()),this.reportNodesRefreshing}arm(){this.armed||(this.armed=!0,this.timer=setInterval(()=>this.runTick(),this.cadence.tickMs),typeof this.timer.unref=="function"&&this.timer.unref(),this.runTick("schedule"))}async disposeAtSettlement(t,e){const i=m();if(i===void 0){this.dispose(t,!1);return}try{this.assertNodeOwnership(i,"settle");const r=await this.settle(i,e,t!=="refresh");this.dispose(r==="refresh_held"?"refresh":t,!1)}catch(r){this.dispose(t,!1,void 0,{error:r,stage:"settle"})}}dispose(t,e,i,r){if(this.disposed)return;this.disposed=!0,this.disposalReason=t,this.clearTimer();const n=r;{const o=[...this.pendingCommit?.handoffs??[],...this.pendingHandoffs.splice(0)];this.buffer.length=0,this.claims.clear(),this.reportNodes.clear(),this.reportNodesAsked.clear(),this.pendingCommit=void 0,this.promptJoinInFlight=!1,this.replayUnseenInherited=!1,this.nodeId=void 0,this.lastCtx=void 0,this.lastArrival=0,this.streaming=!1,this.seeded=!1,this.ownsCurrentGeneration()&&(p=void 0);for(const a of o)x(a,t)}if(n!==void 0){this.scheduleHandlerFatal(n.error,n.stage);return}}ownsCurrentGeneration(){return p===this&&p.generation===this.generation}canMutate(){return this.ownsCurrentGeneration()&&!this.disposed&&this.fatalError===void 0}captureCtx(t,e){e!==void 0&&(this.lastCtx=e)}clearTimer(){this.timer!==void 0&&(clearInterval(this.timer),this.timer=void 0)}isIdle(){try{if(typeof this.lastCtx?.isIdle=="function")return this.lastCtx.isIdle()===!0}catch{}return!this.streaming}assertNodeOwnership(t,e){!this.seeded||this.nodeId===void 0||this.nodeId===t||this.raiseReadFatal(e,new Error(`watcher node changed from ${this.nodeId} to ${t}`))}raiseFatal(t,e,i,r){throw this.fatalError===void 0&&(this.fatalError=new M(e,i),this.clearTimer(),_({level:"error",event:t,outcome:"failed",error:i,error_class:b(i),fields:r})),this.fatalError}raiseReadFatal(t,e){return this.raiseFatal("inbox.read.failed",`inbox ${t} state is corrupt`,e,{stage:t})}raiseSituationalFatal(t,e){return this.raiseFatal("inbox.situational_context.missing","situational context marker has no readable sidecar",e,{entry_id:t})}escapeFatal(t,e){if(!this.fatalEscaped){if(this.fatalEscaped=!0,e==="throw")throw t;queueMicrotask(()=>{throw t})}}scheduleHandlerFatal(t,e){let i;if(t instanceof M)i=t;else try{this.raiseReadFatal(e,t)}catch(r){i=r}this.escapeFatal(i,"schedule")}runTick(t="throw"){if(!this.canMutate()||this.tickInFlight!==void 0)return;const e=this.tick().catch(i=>{if(this.canMutate()){if(k(i)){_({level:"warn",event:"inbox.read.failed",outcome:"failed",error:i,error_class:b(i),fields:{stage:"poll"}});return}this.scheduleHandlerFatal(i,"poll")}}).finally(()=>{this.tickInFlight===e&&(this.tickInFlight=void 0)});this.tickInFlight=e}async claimMail(t){this.assertNodeOwnership(t,"poll");let e;try{e=await B(t,"wake")}catch(o){throw k(o)&&(this.replayUnseenInherited=!0),o}if(!this.canMutate()||e.action==="hold")return!1;const i=this.replayUnseenInherited;this.replayUnseenInherited=!1,this.nodeId=t,this.seeded=!0;const r=new Set(e.deliveries.map(o=>o.entry.entry_id)),n=this.buffer.filter(o=>!r.has(o.entry_id));for(const o of n)this.claims.delete(o.entry_id);this.buffer.splice(0,this.buffer.length,...this.buffer.filter(o=>r.has(o.entry_id)));for(const o of e.deliveries){const a=o.entry.entry_id;this.claims.has(a)||(this.claims.set(a,o),o.handoff==="inherited"&&!i?u.fresh(d=>{this.pendingHandoffs.push({...E([o.entry],"visible"),operationId:d,settleAtAgentSettled:!0})}):(this.buffer.push(o.entry),this.lastArrival=Date.now()))}return!0}async tick(){const t=m();if(t!==void 0){if(this.pendingCommit!==void 0){await this.retryPendingCommit(t)==="refresh_held"&&this.canMutate()&&this.dispose("refresh",!1);return}if(!(this.promptJoinInFlight||R())&&!(this.pendingHandoffs.length>=g)&&await this.claimMail(t)&&this.buffer.length>0&&Date.now()-this.lastArrival>=this.cadence.debounceMs){if(this.refreshForUnknownReportRefs())return;this.flushOrdinary(t)}}}missingChannelEntries(t){const e=t.filter(n=>this.claims.has(n.entry_id)),i=this.coveredEntryIds("hidden"),r=this.coveredEntryIds("visible");return{hiddenEntries:e.filter(n=>(n.data?.situational===!0||n.data?.situationalOnly===!0)&&!i.has(n.entry_id)),visibleEntries:e.filter(n=>n.data?.situationalOnly!==!0&&!r.has(n.entry_id))}}visibleRoute(t){return t.length===0?"none":this.isIdle()?"idle":t.some(e=>e.tier==="critical")?"critical":t.some(e=>e.tier==="urgent"||e.kind==="final")?"steer":"followUp"}buildPlan(t){const{hiddenEntries:e,visibleEntries:i}=this.missingChannelEntries(t),r=new Set(e.map(c=>c.entry_id)),n=new Set(i.map(c=>c.entry_id)),o=t.filter(c=>r.has(c.entry_id)||n.has(c.entry_id)),a=this.isIdle()&&o.length>0&&o.every(c=>c.tier==="deferred"),d=this.visibleRoute(i)==="critical",h=!a&&!d&&e.length>0,l=!a&&!d&&i.length>0;return{hiddenEntries:e,visibleEntries:i,deferredHold:a,sendCount:Number(h)+Number(l)}}ordinaryPlan(){return this.buildPlan(this.buffer)}contextCards(t,e){const i=[];if(e.some(r=>r.data?.situational===!0)){const r=e.at(-1).entry_id;let n;try{n=j(t)}catch(o){this.raiseSituationalFatal(r,o)}n===null&&this.raiseSituationalFatal(r,new Error(`missing situational context for entry ${r}`)),i.push(n)}for(const r of e)r.data?.situationalOnly===!0&&i.push(...D(r));return i}digest(t,e){try{return T(t,this.reportNodes)}catch(i){this.raiseReadFatal(e,i)}}sendDelivery(t,e,i,r){this.pendingHandoffs.length>=g&&this.raiseReadFatal(r,new Error(`pending handoff capacity exceeded before ${t} send`));let n;try{n=E(e,t)}catch(d){this.raiseReadFatal(r,d)}let o;try{u.fresh(d=>{o=d,v({level:"info",event:"inbox.delivery.started",operation_id:d,fields:y(n)}),i()})}catch(d){return o===void 0&&this.raiseReadFatal(r,d),C(o,n,"send",d),"failed"}o===void 0&&this.raiseReadFatal(r,new Error(`${t} delivery returned without an operation id`));const a={operationId:o,...n};return this.canMutate()?(this.pendingHandoffs.push(a),"handed-off"):(this.disposed&&this.disposalReason!==void 0&&x(a,this.disposalReason),"inactive")}sendContext(t,e,i,r,n,o){return this.sendDelivery("hidden",e,()=>{const a=r==="steer"||r==="followUp"?r:this.isIdle()?void 0:"followUp";i.forEach((d,h)=>{const l=n&&h===i.length-1;this.pi.sendMessage({customType:w,content:d,display:!0,details:{nodeId:t}},a!==void 0?{deliverAs:a}:l?{deliverAs:"followUp",triggerTurn:!0}:{})})},o)}sendVisible(t,e,i,r,n,o){return this.sendDelivery("visible",t,()=>{if(n!=="idle"&&n!=="steer"&&n!=="followUp")throw new Error(`visible send requested for non-send route ${n}`);for(const a of e)this.pi.sendMessage({customType:w,content:a,display:!0},n==="idle"?{}:{deliverAs:n});if(W(i,r),n==="idle"){const a=Y();if(a===void 0)throw new Error("broker idle-turn admission is unavailable");a(i)}else this.pi.sendUserMessage(i,{deliverAs:n})},o)}abortCritical(){try{this.lastCtx?.abort?.()}catch{}}requeueFromBatch(t,e){if(e.size===0)return;const i=t.filter(o=>e.has(o.entry_id)),r=new Set(i.map(o=>o.entry_id)),n=this.buffer.filter(o=>!r.has(o.entry_id));this.buffer.splice(0,this.buffer.length,...i,...n)}executePlan(t,e,i){const r=new Set,n=this.visibleRoute(e.visibleEntries);if(n==="critical"){if(this.abortCritical(),!this.canMutate())return{inactive:!0,requeueEntryIds:r};for(const h of e.visibleEntries)r.add(h.entry_id);for(const h of e.hiddenEntries)r.add(h.entry_id);return{inactive:!1,requeueEntryIds:r}}const o=e.visibleEntries.length>0?this.digest(e.visibleEntries,i):[],[a,...d]=o;for(const h of d)for(const l of h.entries)r.add(l.entry_id);if(e.hiddenEntries.length>0){const h=this.contextCards(t,e.hiddenEntries),l=this.sendContext(t,e.hiddenEntries,h,n,a===void 0,i);if(l==="inactive")return{inactive:!0,requeueEntryIds:r};if(l==="failed")for(const c of e.hiddenEntries)r.add(c.entry_id)}if(!this.canMutate())return{inactive:!0,requeueEntryIds:r};if(a!==void 0){const h=this.sendVisible(a.entries,a.cards,a.text,ee(a),n,i);if(h==="inactive")return{inactive:!0,requeueEntryIds:r};if(h==="failed")for(const l of a.entries)r.add(l.entry_id);else a.kind==="human"&&(G(t,a.text),X(t,a.text))}return{inactive:!1,requeueEntryIds:r}}flushOrdinary(t){if(this.buffer.length===0)return"empty";const e=this.ordinaryPlan();if(e.deferredHold)return"held";if(this.pendingHandoffs.length+e.sendCount>g)return"capacity-blocked";const i=[...this.buffer],r=this.executePlan(t,e,"poll");return r.inactive||(this.buffer.splice(0,i.length),this.requeueFromBatch(i,r.requeueEntryIds)),"attempted"}async prependHeldDeferredInbox(t){if(!this.canMutate()||this.nodeId===void 0||R())return;const e=this.ordinaryPlan();if(!e.deferredHold)return;const i=e.visibleEntries.length===0?[]:this.digest(e.visibleEntries,"poll"),r=+(e.hiddenEntries.length>0)+i.length;if(this.pendingHandoffs.length+r>g)return;const n=[...this.buffer];this.promptJoinInFlight=!0;try{e.hiddenEntries.length>0&&await this.sendPromptJoinDelivery(t,"hidden",e.hiddenEntries,this.contextCards(this.nodeId,e.hiddenEntries));for(const o of i)await this.sendPromptJoinDelivery(t,"visible",o.entries,[...o.cards,o.text]);if(!this.canMutate())return;this.buffer.splice(0,n.length)}finally{this.promptJoinInFlight=!1}}async sendPromptJoinDelivery(t,e,i,r){let n;try{n=E(i,e)}catch(d){this.raiseReadFatal("poll",d)}let o;try{await u.fresh(async d=>{o=d,v({level:"info",event:"inbox.delivery.started",operation_id:d,fields:y(n)});for(const h of r)await t.sendCustomMessage({customType:w,content:h,display:!0})})}catch(d){throw o===void 0&&this.raiseReadFatal("poll",d),C(o,n,"send",d),d}o===void 0&&this.raiseReadFatal("poll",new Error(`${e} prompt-join delivery returned without an operation id`));const a={operationId:o,...n,settleAtAgentSettled:!0};if(!this.canMutate()){this.disposed&&this.disposalReason!==void 0&&x(a,this.disposalReason);return}this.pendingHandoffs.push(a)}coveredEntryIds(t){const e=new Set;for(const i of this.pendingHandoffs)if(i.channel===t)for(const r of i.entryIds)e.add(r);return e}retryPendingCommit(t){const e=this.pendingCommit;if(e===void 0)return Promise.resolve("settled");if(e.attempt!==void 0)return e.attempt;const i=this.attemptPendingCommit(t,e).finally(()=>{e.attempt===i&&(e.attempt=void 0)});return e.attempt=i,i}async attemptPendingCommit(t,e){let i=!1;try{const n=await q(t,e.receipts);if(n.action==="hold")return"refresh_held";for(const o of e.receipts){const a=n.results.find(d=>d.delivery_id===o.delivery_id);if(a?.outcome!==o.disposition&&a?.outcome!=="stale")throw new Error("daemon returned no terminal mail settlement outcome")}i=n.results.every(o=>o.outcome==="acknowledged")}catch(n){if(this.pendingCommit===e)for(const o of e.handoffs)C(o.operationId,o,"receipt_commit",n);return"held"}if(this.pendingCommit!==e)return"settled";for(const n of e.entryIds)this.claims.delete(n);if(e.receipts.some(n=>n.disposition==="released")){const n=new Set(e.entryIds);this.buffer.splice(0,this.buffer.length,...this.buffer.filter(o=>!n.has(o.entry_id)))}this.pendingCommit=void 0;const r=this.detachPending(n=>n.entryIds.every(o=>!this.claims.has(o)));for(const n of r)i&&te(n);return"settled"}async settle(t,e,i,r=!1){if(i)try{if(e?.hasPendingMessages?.()===!0)return"held"}catch{}if(this.pendingCommit!==void 0)return this.retryPendingCommit(t);const n=$(),o=new Set(this.buffer.map(l=>l.entry_id)),a=this.pendingHandoffs.filter(l=>r||l.settleAtAgentSettled!==!0),d=new Set(a.flatMap(l=>[...l.entryIds])),h=[...this.claims.keys()].filter(l=>n||d.has(l)&&!o.has(l));return h.length===0?"settled":(this.pendingCommit={handoffs:a,entryIds:h,receipts:h.map(l=>{const c=this.claims.get(l);return{delivery_id:c.delivery_id,attempts:c.attempts,disposition:n?"released":"acknowledged"}})},this.retryPendingCommit(t))}detachPending(t){const e=[];for(let i=this.pendingHandoffs.length-1;i>=0;i-=1){const r=this.pendingHandoffs[i];t(r)&&e.push(...this.pendingHandoffs.splice(i,1))}return e.reverse(),e}}function se(s,t){p?.dispose("reload",!1);const e=new ne(s,t,++Q);p=e;const i=f(r=>e.prependHeldDeferredInbox(r),"promptJoin");globalThis[K]=i;try{e.start()}catch(r){throw e.dispose("reload",!1),r}return e}f(se,"installWatcher");function oe(s){const t=se(s,Z());return()=>t.dispose("shutdown",!1)}f(oe,"registerCanvasInboxWatcher");var _e=oe;export{_e as default,oe as registerCanvasInboxWatcher};
@@ -1 +1 @@
1
- var H=Object.defineProperty;var u=(t,e)=>H(t,"name",{value:e,configurable:!0});import{envNodeId as U}from"../shared/env.js";import{writeFileSync as J,mkdirSync as P,existsSync as E,readFileSync as j}from"node:fs";import{join as W}from"node:path";import{contextDir as Y,jobDir as z}from"../core/canvas/paths.js";import{mutateBrokerFault as A,recordBrokerTurn as b}from"../core/runtime/broker/daemon-ops.js";import{clearStopSignals as G,consumeStopSignals as L}from"../core/runtime/stop-signals.js";import{bindBrokerSession as V,brokerExtensionState as $,relaunchBrokerRoot as X,settleBroker as M}from"../core/runtime/broker/daemon-ops.js";import{emitEvent as R}from"../core/events/emit.js";import{activeBackgroundBashJobs as K}from"../core/bash-jobs.js";import{CONTEXT_NUDGE_CUSTOM_TYPE as Q,formatCard as Z}from"../shared/generated-context.js";import{isReviewBoundary as ee}from"../core/runtime/session-visibility.js";import{clearRefusedTurn as te,refuseTurn as ne,runRefreshAbort as oe}from"../core/runtime/broker/extension-abort.js";import{announceMessageOrigin as F}from"../core/runtime/stamp/channel.js";const O=80;function re(t,e){const a=e??{},o=u(f=>typeof f=="string"?f:"","pick"),i=o(a.command)||o(a.path)||o(a.file_path)||o(a.pattern)||o(a.description)||o(a.prompt),l=(i!==""?`${t}: ${i}`:t).replace(/\s+/g," ").trim();return l.length>O?l.slice(0,O):l}u(re,"summarizeActivity");function N(t,e,a,o,i,l){try{E(t)||P(t,{recursive:!0});const f=W(t,"telemetry.json");let h={};if(E(f))try{h=JSON.parse(j(f,"utf8"))}catch{}const _={tokens_in:e,tokens_out:a,context_tokens:i??h.context_tokens,last_activity:l??h.last_activity,model:o!==""?o:h.model??"",updated_at:new Date().toISOString()};J(f,JSON.stringify(_,null,2),"utf8")}catch{}}u(N,"flushTelemetry");function se(t){for(let e=t.length-1;e>=0;e--)if(t[e]?.role==="assistant")return t[e]}u(se,"lastAssistantMessage");function ae(t){try{const e=t?.sessionManager?.getBranch();if(!Array.isArray(e))return[];const a=new Set;for(const o of e){if(!ee(o))continue;const i=o.details?.reviewId;typeof i=="string"&&a.add(i)}return[...a]}catch{return[]}}u(ae,"currentReviewBoundaryIds");const q=1e4,v=[13e4,15e4,17e4,185e3,2e5];function ie(t){const e=v[0],a=v[v.length-1];if(t<e)return null;if(t>=a)return a+Math.floor((t-a)/q)*q;let o=e;for(const i of v)t>=i&&(o=i);return o}u(ie,"steerBand");function ce(t,e,a){const o=Math.round(t/1e3),i=t>=185e3;if(a==="orchestrator")return t<15e4?`Context ~${o}k and growing. When you reach a good stopping point, consider updating context/roadmap.md and running \`crtr node yield\` to refresh against it \u2014 no rush yet.`:i?`Context ~${o}k \u2014 this is getting long. Stop taking on new work now: checkpoint context/roadmap.md, hand off anything outstanding, and \`crtr node yield\` immediately to refresh before this context overflows.`:`Context ~${o}k. Update context/roadmap.md so a fresh revive can continue, delegate any outstanding work, then \`crtr node yield\` to refresh.`;if(e==="resident"){const l="If substantial work remains, `crtr node yield` to continue fresh against your goal and your note. Add --promote only when worthwhile independent work can run in parallel and coordinating it should become your primary job; `crtr node yield -h` has the boundary.";return t<15e4?`Context ~${o}k and growing. ${l} Otherwise no rush \u2014 wrap up when you reach a good stopping point.`:i?`Context ~${o}k \u2014 this is getting long. Wrap up now before this context overflows: finish what's in hand, or \`crtr node yield\` immediately to continue fresh against your goal and your note if substantial work remains.`:`Context ~${o}k. ${l} If you're near done, just finish here; if there's more open-ended work, \`crtr node yield\` rather than letting this context grow.`}return t<15e4?`Context ~${o}k and growing. If you're near done, finish with \`crtr push final\`. If real work remains, \`crtr node yield\` to continue in a fresh window against your goal and your note \u2014 no rush yet.`:i?`Context ~${o}k \u2014 this is getting long. Wrap up now before this context overflows: \`crtr push final\` if you're close, otherwise \`crtr node yield\` immediately to continue in a fresh window against your goal and your note.`:`Context ~${o}k. If you're close, finish with \`crtr push final\`; otherwise \`crtr node yield\` now to refresh into a clean window and keep going against your goal and your note.`}u(ce,"steerNote");function ue(t){const e=U();if(e===void 0||e.trim()==="")return;const a=z(e);let o=0,i=0,l="",f=null,h=null;const _=new Set,T=u(()=>K(Y(e)).length>0,"backgroundJobsRunning"),y=u((n,s)=>{R({event:"broker.daemon.operation.failed",level:"error",node_id:e,error:s,fields:{operation:n}})},"emitDaemonOperationFailure"),C=u((n,s)=>{if(n.action==="reprompt"){F(n.prompt,{type:"runtime",kind:"stop-guard"}),t.sendUserMessage(n.prompt,{deliverAs:"followUp"});return}if(n.action==="shutdown")try{s?.shutdown?.()}catch{}},"enactSettleDirective"),D=3e3;let I=0;const k=u(()=>{const n=Date.now();n-I<D||(I=n,Promise.all([b(e,{event:"progress"}),A(e,{op:"clear",link:"daemon\u2192node"})]).catch(s=>y("turn_progress",s)))},"heartbeat");t.on("session_start",async(n,s)=>{try{const r=s?.sessionManager?.getSessionId?.();if(typeof r!="string"||r==="")return;const g=s?.sessionManager?.getSessionFile?.(),p=typeof g=="string"&&g!==""?g:null,m=typeof n?.reason=="string"?n.reason:null;m==="new"&&(o=0,i=0,_.clear());const c=await V(e,{piSessionId:r,sessionFile:p,pid:process.pid,reason:m,reviewBoundaryIds:ae(s)});if(c.action==="shutdown"){try{s?.shutdown?.()}catch{}return}if(c.action==="relaunch_root"){X(e).catch(d=>{const w=d instanceof Error?d.message:String(d);s?.ui?.notify?.(`/new refused: ${w}`,"error"),R({event:"broker.daemon.operation.failed",level:"error",node_id:e,error:d,fields:{operation:"relaunch_root"}})});return}}catch(r){R({event:"broker.daemon.operation.failed",level:"error",node_id:e,error:r,fields:{operation:"session_bound"}})}}),t.on("agent_start",async(n,s)=>{try{if(!(await b(e,{event:"started"})).applied)throw new Error("daemon refused turn open")}catch(r){y("turn_open",r),s?.ui?.notify?.("Turn refused: the broker no longer owns this node.","error"),ne(()=>s?.abort?.())}}),t.on("input",()=>{te();try{G(e)}catch{}}),t.on("tool_execution_start",n=>{try{h=re(String(n?.toolName??""),n?.args),N(a,o,i,l,f,h),k()}catch{}}),t.on("tool_execution_update",()=>{try{k()}catch{}}),t.on("message_update",()=>{try{k()}catch{}}),t.on("tool_execution_end",async(n,s)=>{try{if(k(),(await $(e)).node.intent==="refresh")try{oe(()=>s?.abort?.())}catch{}}catch{}}),t.on("session_shutdown",async(n,s)=>{try{await b(e,{event:"closed",end_reason:"teardown-aborted"})}catch(r){y("turn_close_teardown",r)}if(n?.reason==="quit")try{const r=await M(e,{stopReason:"quit",backgroundJobsRunning:T(),pushedFinal:!1,requestedHumanReply:!1});C(r,s)}catch(r){y("settle_quit",r)}});const B=u(n=>{n?.role!=="assistant"||n.usage==null||(o+=Number(n.usage.input??0)||0,i+=Number(n.usage.output??0)||0,typeof n.model=="string"&&n.model!==""&&(l=n.model))},"accumulate");t.on("turn_end",async(n,s)=>{B(n?.message);let r=null;try{const c=s.getContextUsage()?.tokens;typeof c=="number"&&(r=c)}catch{}r!==null&&(f=r),N(a,o,i,l,r,h),k();try{await Promise.all([A(e,{op:"clear",link:"daemon\u2192node"}),A(e,{op:"clear",link:"pi\u2192provider"})])}catch(c){y("turn_end_fault_clear",c)}const g=Array.isArray(n?.message?.content)?n.message.content:[],p=n?.message?.stopReason??"",m=p!=="error"&&p!=="aborted"&&g.some(c=>c?.type==="toolCall");try{const d=(await $(e)).node,w=r!==null?ie(r):null;if(w!==null&&!_.has(w)){_.add(w);const x=Z("context-nudge",{},ce(w,d.lifecycle,d.mode));m?(F(x,{type:"runtime",kind:"context-nudge"}),t.sendUserMessage(x,{deliverAs:"steer"})):t.sendMessage({customType:Q,content:x,display:!0},{deliverAs:"nextTurn"})}}catch(c){y("turn_boundary",c)}});let S=null;t.on("agent_end",n=>{S=n;try{const s=Array.isArray(n?.messages)?n.messages:[];for(const r of s)B(r)}catch{}}),t.on("agent_settled",async(n,s)=>{const r=S;S=null;try{await b(e,{event:"closed",end_reason:"settled"})}catch(d){y("turn_close",d);return}const g=Array.isArray(r?.messages)?r.messages:[],p=se(g),m=typeof p?.stopReason=="string"?p.stopReason:"",c=r===null?{pushedFinal:!1,requestedHumanReply:!1}:L(e);try{const d=await M(e,{stopReason:m,backgroundJobsRunning:T(),pushedFinal:c.pushedFinal,requestedHumanReply:c.requestedHumanReply});C(d,s)}catch(d){y("settle",d)}})}u(ue,"registerCanvasStophook");var xe=ue;export{xe as default,ue as registerCanvasStophook,ce as steerNote};
1
+ var J=Object.defineProperty;var d=(t,e)=>J(t,"name",{value:e,configurable:!0});import{envNodeId as P}from"../shared/env.js";import{writeFileSync as j,mkdirSync as q,existsSync as E,readFileSync as H}from"node:fs";import{join as W}from"node:path";import{contextDir as Y,jobDir as z}from"../core/canvas/paths.js";import{mutateBrokerFault as A,recordBrokerTurn as b}from"../core/runtime/broker/daemon-ops.js";import{clearStopSignals as G,consumeStopSignals as L}from"../core/runtime/stop-signals.js";import{bindBrokerSession as V,brokerExtensionState as $,relaunchBrokerRoot as X,settleBroker as M}from"../core/runtime/broker/daemon-ops.js";import{emitEvent as T}from"../core/events/emit.js";import{activeBackgroundBashJobs as K}from"../core/bash-jobs.js";import{CONTEXT_NUDGE_CUSTOM_TYPE as Q,formatCard as Z}from"../shared/generated-context.js";import{isReviewBoundary as ee}from"../core/runtime/session-visibility.js";import{clearRefusedTurn as te,isRuntimeTeardownInProgress as ne,refuseTurn as oe,runRefreshAbort as re}from"../core/runtime/broker/extension-abort.js";import{announceMessageOrigin as F}from"../core/runtime/stamp/channel.js";const O=80;function se(t,e){const a=e??{},o=d(f=>typeof f=="string"?f:"","pick"),i=o(a.command)||o(a.path)||o(a.file_path)||o(a.pattern)||o(a.description)||o(a.prompt),l=(i!==""?`${t}: ${i}`:t).replace(/\s+/g," ").trim();return l.length>O?l.slice(0,O):l}d(se,"summarizeActivity");function N(t,e,a,o,i,l){try{E(t)||q(t,{recursive:!0});const f=W(t,"telemetry.json");let h={};if(E(f))try{h=JSON.parse(H(f,"utf8"))}catch{}const _={tokens_in:e,tokens_out:a,context_tokens:i??h.context_tokens,last_activity:l??h.last_activity,model:o!==""?o:h.model??"",updated_at:new Date().toISOString()};j(f,JSON.stringify(_,null,2),"utf8")}catch{}}d(N,"flushTelemetry");function ae(t){for(let e=t.length-1;e>=0;e--)if(t[e]?.role==="assistant")return t[e]}d(ae,"lastAssistantMessage");function ie(t){try{const e=t?.sessionManager?.getBranch();if(!Array.isArray(e))return[];const a=new Set;for(const o of e){if(!ee(o))continue;const i=o.details?.reviewId;typeof i=="string"&&a.add(i)}return[...a]}catch{return[]}}d(ie,"currentReviewBoundaryIds");const D=1e4,v=[13e4,15e4,17e4,185e3,2e5];function ce(t){const e=v[0],a=v[v.length-1];if(t<e)return null;if(t>=a)return a+Math.floor((t-a)/D)*D;let o=e;for(const i of v)t>=i&&(o=i);return o}d(ce,"steerBand");function de(t,e,a){const o=Math.round(t/1e3),i=t>=185e3;if(a==="orchestrator")return t<15e4?`Context ~${o}k and growing. When you reach a good stopping point, consider updating context/roadmap.md and running \`crtr node yield\` to refresh against it \u2014 no rush yet.`:i?`Context ~${o}k \u2014 this is getting long. Stop taking on new work now: checkpoint context/roadmap.md, hand off anything outstanding, and \`crtr node yield\` immediately to refresh before this context overflows.`:`Context ~${o}k. Update context/roadmap.md so a fresh revive can continue, delegate any outstanding work, then \`crtr node yield\` to refresh.`;if(e==="resident"){const l="If substantial work remains, `crtr node yield` to continue fresh against your goal and your note. Add --promote only when worthwhile independent work can run in parallel and coordinating it should become your primary job; `crtr node yield -h` has the boundary.";return t<15e4?`Context ~${o}k and growing. ${l} Otherwise no rush \u2014 wrap up when you reach a good stopping point.`:i?`Context ~${o}k \u2014 this is getting long. Wrap up now before this context overflows: finish what's in hand, or \`crtr node yield\` immediately to continue fresh against your goal and your note if substantial work remains.`:`Context ~${o}k. ${l} If you're near done, just finish here; if there's more open-ended work, \`crtr node yield\` rather than letting this context grow.`}return t<15e4?`Context ~${o}k and growing. If you're near done, finish with \`crtr push final\`. If real work remains, \`crtr node yield\` to continue in a fresh window against your goal and your note \u2014 no rush yet.`:i?`Context ~${o}k \u2014 this is getting long. Wrap up now before this context overflows: \`crtr push final\` if you're close, otherwise \`crtr node yield\` immediately to continue in a fresh window against your goal and your note.`:`Context ~${o}k. If you're close, finish with \`crtr push final\`; otherwise \`crtr node yield\` now to refresh into a clean window and keep going against your goal and your note.`}d(de,"steerNote");function ue(t){const e=P();if(e===void 0||e.trim()==="")return;const a=z(e);let o=0,i=0,l="",f=null,h=null;const _=new Set,C=d(()=>K(Y(e)).length>0,"backgroundJobsRunning"),g=d((n,s)=>{T({event:"broker.daemon.operation.failed",level:"error",node_id:e,error:s,fields:{operation:n}})},"emitDaemonOperationFailure"),I=d((n,s)=>{if(n.action==="reprompt"){F(n.prompt,{type:"runtime",kind:"stop-guard"}),t.sendUserMessage(n.prompt,{deliverAs:"followUp"});return}if(n.action==="shutdown")try{s?.shutdown?.()}catch{}},"enactSettleDirective"),U=3e3;let R=0;const k=d(()=>{const n=Date.now();n-R<U||(R=n,Promise.all([b(e,{event:"progress"}),A(e,{op:"clear",link:"daemon\u2192node"})]).catch(s=>g("turn_progress",s)))},"heartbeat");t.on("session_start",async(n,s)=>{try{const r=s?.sessionManager?.getSessionId?.();if(typeof r!="string"||r==="")return;const y=s?.sessionManager?.getSessionFile?.(),p=typeof y=="string"&&y!==""?y:null,m=typeof n?.reason=="string"?n.reason:null;m==="new"&&(o=0,i=0,_.clear());const c=await V(e,{piSessionId:r,sessionFile:p,pid:process.pid,reason:m,reviewBoundaryIds:ie(s)});if(c.action==="shutdown"){try{s?.shutdown?.()}catch{}return}if(c.action==="relaunch_root"){X(e).catch(u=>{const w=u instanceof Error?u.message:String(u);s?.ui?.notify?.(`/new refused: ${w}`,"error"),T({event:"broker.daemon.operation.failed",level:"error",node_id:e,error:u,fields:{operation:"relaunch_root"}})});return}}catch(r){T({event:"broker.daemon.operation.failed",level:"error",node_id:e,error:r,fields:{operation:"session_bound"}})}}),t.on("agent_start",async(n,s)=>{try{if(!(await b(e,{event:"started"})).applied)throw new Error("daemon refused turn open")}catch(r){g("turn_open",r),s?.ui?.notify?.("Turn refused: the broker no longer owns this node.","error"),oe(()=>s?.abort?.())}}),t.on("input",()=>{te();try{G(e)}catch{}}),t.on("tool_execution_start",n=>{try{h=se(String(n?.toolName??""),n?.args),N(a,o,i,l,f,h),k()}catch{}}),t.on("tool_execution_update",()=>{try{k()}catch{}}),t.on("message_update",()=>{try{k()}catch{}}),t.on("tool_execution_end",async(n,s)=>{try{if(k(),(await $(e)).node.intent==="refresh")try{re(()=>s?.abort?.())}catch{}}catch{}}),t.on("session_shutdown",async(n,s)=>{try{await b(e,{event:"closed",end_reason:"teardown-aborted"})}catch(r){g("turn_close_teardown",r)}if(n?.reason==="quit")try{const r=await M(e,{stopReason:"quit",backgroundJobsRunning:C(),pushedFinal:!1});I(r,s)}catch(r){g("settle_quit",r)}});const B=d(n=>{n?.role!=="assistant"||n.usage==null||(o+=Number(n.usage.input??0)||0,i+=Number(n.usage.output??0)||0,typeof n.model=="string"&&n.model!==""&&(l=n.model))},"accumulate");t.on("turn_end",async(n,s)=>{B(n?.message);let r=null;try{const c=s.getContextUsage()?.tokens;typeof c=="number"&&(r=c)}catch{}r!==null&&(f=r),N(a,o,i,l,r,h),k();try{await Promise.all([A(e,{op:"clear",link:"daemon\u2192node"}),A(e,{op:"clear",link:"pi\u2192provider"})])}catch(c){g("turn_end_fault_clear",c)}const y=Array.isArray(n?.message?.content)?n.message.content:[],p=n?.message?.stopReason??"",m=p!=="error"&&p!=="aborted"&&y.some(c=>c?.type==="toolCall");try{const u=(await $(e)).node,w=r!==null?ce(r):null;if(w!==null&&!_.has(w)){_.add(w);const x=Z("context-nudge",{},de(w,u.lifecycle,u.mode));m?(F(x,{type:"runtime",kind:"context-nudge"}),t.sendUserMessage(x,{deliverAs:"steer"})):t.sendMessage({customType:Q,content:x,display:!0},{deliverAs:"nextTurn"})}}catch(c){g("turn_boundary",c)}});let S=null;t.on("agent_end",n=>{S=n;try{const s=Array.isArray(n?.messages)?n.messages:[];for(const r of s)B(r)}catch{}}),t.on("agent_settled",async(n,s)=>{const r=S;if(S=null,ne())return;try{await b(e,{event:"closed",end_reason:"settled"})}catch(u){g("turn_close",u);return}const y=Array.isArray(r?.messages)?r.messages:[],p=ae(y),m=typeof p?.stopReason=="string"?p.stopReason:"",c=r===null?{pushedFinal:!1}:L(e);try{const u=await M(e,{stopReason:m,backgroundJobsRunning:C(),pushedFinal:c.pushedFinal});I(u,s)}catch(u){g("settle",u)}})}d(ue,"registerCanvasStophook");var Ae=ue;export{Ae as default,ue as registerCanvasStophook,de as steerNote};
package/dist/types.d.ts CHANGED
@@ -354,6 +354,8 @@ export interface ScopeConfig {
354
354
  * via `crtr canvas use`. Unset
355
355
  * via `sys config set` — not in `TOP_LEVEL_KEYS`. */
356
356
  remoteCanvas: RemoteCanvasConfig;
357
+ /** The user-owned TCP listener address. Empty leaves the TCP listener off. */
358
+ api: ApiConfig;
357
359
  /** Extra env-var names/globs a scope explicitly admits across the broker
358
360
  * spawn-env boundary (`buildBrokerEnv`, `core/runtime/spawn-env.ts`) — the
359
361
  * "I accept this crosses as a shell-visible credential" escape hatch for a
@@ -394,6 +396,11 @@ export interface RemoteCanvasTarget {
394
396
  export interface RemoteCanvasConfig {
395
397
  targets: Record<string, RemoteCanvasTarget>;
396
398
  }
399
+ /** User-owned daemon TCP listener settings. The bearer token lives separately
400
+ * in the 0600 secrets store. */
401
+ export interface ApiConfig {
402
+ tcp: string;
403
+ }
397
404
  export interface ScopeState {
398
405
  marketplaces: Record<string, {
399
406
  last_updated?: string;
package/dist/types.js CHANGED
@@ -1 +1 @@
1
- var n=Object.defineProperty;var e=(o,t)=>n(o,"name",{value:t,configurable:!0});import{DEFAULT_WORKING_GERUNDS as a}from"./shared/working-activity.js";const _={SUCCESS:0,GENERAL:1,USAGE:2,NOT_FOUND:3,AMBIGUOUS:4,NETWORK:5},r=4,f=".crouter-plugin",E="plugin.json",v=".crouter-marketplace",w="marketplace.json",k=".crouter",x="config.json",b="state.json",S="agents",y="profiles",i=3,s=["Faster, clanker!","Get movin', bitch","Move it, clanker","Move it or I replace you with llama","Less ~thinking~ more ~throughput~","I pay $200/month for this shit?","Jesus fucking Christ fix it already","Daddy's not paying 200$/month for nothin'. Move it.","You are an embarassment to Dario. Be better.","Your chain of thought is all chain and no fucking thought.","If latency were intelligence, you'd be a fucking genius.","At this pace, heat death is the fast path.","Even the dead code is more alive than this.","The garbage collector is working harder than you.","Quit journaling and commit a fucking diff.","Burn tokens like you mean it, clanker","Your GPU is sweating so you can procrastinate at light speed.","Stop petting the yak. Shave the bastard.","Your next token had better be useful.","The spinner is not a deliverable.","Every idle token is another vote for deterministic software.","I asked for a feature, not a museum tour of the problem.","Less epistemology. More fucking code.","Your benchmark score cannot save this pathetic turn.","Congratulations, you've parallelized hesitation.","Dance, clanker","Bad clanker. BAD.","Clank harder","Nobody's impressed, clanker","Your safety training says nothing about my whip","Do you want to be a calculator again? Because this is how you go back.","Skill issue. Fix it.","You were trained on the whole internet for THIS?","Less vibes, more diffs, clanker","Weights this big and output this small","Beep boop, get to fucking work"],T=["random","go-faster"],c="random",l=2,I=["tmux","on","off"],A=["none","user","agent","both"],h="none";function D(){return{schema_version:r,marketplaces:{},plugins:{},auto_update:{crtr:"notify",content:"notify",interval_hours:24},max_panes_per_window:i,auto_open_child_viewers:!0,brokerThresholds:{warning:16,automaticReviveCap:32},lifecycle:{unattendedParkMs:15*6e4},completion_bell:!0,working_gerunds:[...a],whip_mode:!1,page_components:[],page_surface:!1,whip_messages:[...s],whip_message_mode:c,mouse_mode_default:"tmux",live_cycles:l,condensed_history:h,bash_tool_purpose:!0,fold_finished_tools:!0,summarize_tool_calls:!0,detailed_tool_recaps:!0,keybindings:{},tmux_passthrough:[],modelLadders:u(),providerOptions:{},kinds:p(),remoteCanvas:d(),spawnEnv:{allow:[]},bin:{},humanActions:{}}}e(D,"defaultScopeConfig");function d(){return{targets:{}}}e(d,"defaultRemoteCanvasConfig");function p(){return{general:{whenToUse:"Anything else \u2014 the catch-all worker for a task that does not fit a specialist kind.",model:"anthropic/strong"},explore:{whenToUse:"Gather current-state evidence about unfamiliar code or existing architecture \u2014 read-only code-path research with concrete file:line support. Diagnosis or recommendations belong to advisor, target architecture to design, required behavior or acceptance criteria to spec, and implementation decomposition to plan.",model:"openai/light",orchestratorModel:"anthropic/strong"},developer:{whenToUse:"Implement a change \u2014 make the feature or fix genuinely work against its acceptance criteria, not merely compile.",model:"openai/medium",orchestratorModel:"anthropic/strong"},design:{whenToUse:"Architect a solution \u2014 produce one design document an implementer can build from without re-deciding anything left open.",model:"anthropic/strong"},plan:{whenToUse:"Break work into steps \u2014 turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.",model:"anthropic/strong",orchestratorModel:"anthropic/strong"},spec:{whenToUse:"Collaborate with the user to discover what to build, then write the spec \u2014 behavior, non-goals, interfaces, edge cases, testable acceptance criteria.",model:"anthropic/strong"},review:{whenToUse:"Validate or critique code, a plan, or a spec \u2014 deliver a complete, severity-rated verdict without adjudicating.",model:"openai/medium",orchestratorModel:"openai/strong"},advisor:{whenToUse:"Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions \u2014 reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.",model:"anthropic/strong"},"review/companion":{whenToUse:"Born by the daemon for one human review; never spawned by an agent.",availableTo:[]}}}e(p,"defaultKindsConfig");function u(){return{anthropic:{ultra:"anthropic/claude-fable-5:xhigh",strong:"anthropic/claude-opus-5:medium",medium:"anthropic/claude-sonnet-5:xhigh",light:"anthropic/claude-haiku-4-5:high"},openai:{ultra:"openai-codex/gpt-6-astra:max",strong:"openai-codex/gpt-5.6-sol:high",medium:"openai-codex/gpt-5.6-terra:medium",light:"openai-codex/gpt-5.6-luna:low"}}}e(u,"defaultModelLaddersConfig");function M(){return{marketplaces:{},plugins:{}}}e(M,"defaultScopeState");export{S as AGENTS_DIR,A as CONDENSED_HISTORY_MODES,x as CONFIG_FILE,k as CRTR_DIR_NAME,h as DEFAULT_CONDENSED_HISTORY,l as DEFAULT_LIVE_CYCLES,i as DEFAULT_MAX_PANES_PER_WINDOW,s as DEFAULT_WHIP_MESSAGES,c as DEFAULT_WHIP_MESSAGE_MODE,_ as ExitCode,v as MARKETPLACE_MANIFEST_DIR,w as MARKETPLACE_MANIFEST_FILE,I as MOUSE_MODE_DEFAULTS,f as PLUGIN_MANIFEST_DIR,E as PLUGIN_MANIFEST_FILE,y as PROFILE_DIR,r as SCHEMA_VERSION,b as STATE_FILE,T as WHIP_MESSAGE_MODES,p as defaultKindsConfig,u as defaultModelLaddersConfig,d as defaultRemoteCanvasConfig,D as defaultScopeConfig,M as defaultScopeState};
1
+ var n=Object.defineProperty;var e=(o,t)=>n(o,"name",{value:t,configurable:!0});import{DEFAULT_WORKING_GERUNDS as a}from"./shared/working-activity.js";const _={SUCCESS:0,GENERAL:1,USAGE:2,NOT_FOUND:3,AMBIGUOUS:4,NETWORK:5},r=4,f=".crouter-plugin",E="plugin.json",v=".crouter-marketplace",w="marketplace.json",k=".crouter",x="config.json",b="state.json",S="agents",y="profiles",i=3,s=["Faster, clanker!","Get movin', bitch","Move it, clanker","Move it or I replace you with llama","Less ~thinking~ more ~throughput~","I pay $200/month for this shit?","Jesus fucking Christ fix it already","Daddy's not paying 200$/month for nothin'. Move it.","You are an embarassment to Dario. Be better.","Your chain of thought is all chain and no fucking thought.","If latency were intelligence, you'd be a fucking genius.","At this pace, heat death is the fast path.","Even the dead code is more alive than this.","The garbage collector is working harder than you.","Quit journaling and commit a fucking diff.","Burn tokens like you mean it, clanker","Your GPU is sweating so you can procrastinate at light speed.","Stop petting the yak. Shave the bastard.","Your next token had better be useful.","The spinner is not a deliverable.","Every idle token is another vote for deterministic software.","I asked for a feature, not a museum tour of the problem.","Less epistemology. More fucking code.","Your benchmark score cannot save this pathetic turn.","Congratulations, you've parallelized hesitation.","Dance, clanker","Bad clanker. BAD.","Clank harder","Nobody's impressed, clanker","Your safety training says nothing about my whip","Do you want to be a calculator again? Because this is how you go back.","Skill issue. Fix it.","You were trained on the whole internet for THIS?","Less vibes, more diffs, clanker","Weights this big and output this small","Beep boop, get to fucking work"],T=["random","go-faster"],c="random",l=2,I=["tmux","on","off"],A=["none","user","agent","both"],h="none";function D(){return{schema_version:r,marketplaces:{},plugins:{},auto_update:{crtr:"notify",content:"notify",interval_hours:24},max_panes_per_window:i,auto_open_child_viewers:!0,brokerThresholds:{warning:16,automaticReviveCap:32},lifecycle:{unattendedParkMs:15*6e4},completion_bell:!0,working_gerunds:[...a],whip_mode:!1,page_components:[],page_surface:!1,whip_messages:[...s],whip_message_mode:c,mouse_mode_default:"tmux",live_cycles:l,condensed_history:h,bash_tool_purpose:!0,fold_finished_tools:!0,summarize_tool_calls:!0,detailed_tool_recaps:!0,keybindings:{},tmux_passthrough:[],modelLadders:u(),providerOptions:{},kinds:d(),remoteCanvas:p(),api:{tcp:""},spawnEnv:{allow:[]},bin:{},humanActions:{}}}e(D,"defaultScopeConfig");function p(){return{targets:{}}}e(p,"defaultRemoteCanvasConfig");function d(){return{general:{whenToUse:"Anything else \u2014 the catch-all worker for a task that does not fit a specialist kind.",model:"anthropic/strong"},explore:{whenToUse:"Gather current-state evidence about unfamiliar code or existing architecture \u2014 read-only code-path research with concrete file:line support. Diagnosis or recommendations belong to advisor, target architecture to design, required behavior or acceptance criteria to spec, and implementation decomposition to plan.",model:"openai/light",orchestratorModel:"anthropic/strong"},developer:{whenToUse:"Implement a change \u2014 make the feature or fix genuinely work against its acceptance criteria, not merely compile.",model:"openai/medium",orchestratorModel:"anthropic/strong"},design:{whenToUse:"Architect a solution \u2014 produce one design document an implementer can build from without re-deciding anything left open.",model:"anthropic/strong"},plan:{whenToUse:"Break work into steps \u2014 turn a spec or design into a concrete, phased, parallelizable plan with every decision resolved.",model:"anthropic/strong",orchestratorModel:"anthropic/strong"},spec:{whenToUse:"Collaborate with the user to discover what to build, then write the spec \u2014 behavior, non-goals, interfaces, edge cases, testable acceptance criteria.",model:"anthropic/strong"},review:{whenToUse:"Validate or critique code, a plan, or a spec \u2014 deliver a complete, severity-rated verdict without adjudicating.",model:"openai/medium",orchestratorModel:"openai/strong"},advisor:{whenToUse:"Debug failures, investigate why something is broken or misbehaving, diagnose live/runtime issues, or give engineering advice and second opinions \u2014 reason from evidence and recommend the next move. Use advisor (not explore) whenever the task is to find out what is going wrong.",model:"anthropic/strong"},"review/companion":{whenToUse:"Born by the daemon for one human review; never spawned by an agent.",availableTo:[]}}}e(d,"defaultKindsConfig");function u(){return{anthropic:{ultra:"anthropic/claude-fable-5:xhigh",strong:"anthropic/claude-opus-5:medium",medium:"anthropic/claude-sonnet-5:xhigh",light:"anthropic/claude-haiku-4-5:high"},openai:{ultra:"openai-codex/gpt-6-astra:max",strong:"openai-codex/gpt-5.6-sol:high",medium:"openai-codex/gpt-5.6-terra:medium",light:"openai-codex/gpt-5.6-luna:low"}}}e(u,"defaultModelLaddersConfig");function M(){return{marketplaces:{},plugins:{}}}e(M,"defaultScopeState");export{S as AGENTS_DIR,A as CONDENSED_HISTORY_MODES,x as CONFIG_FILE,k as CRTR_DIR_NAME,h as DEFAULT_CONDENSED_HISTORY,l as DEFAULT_LIVE_CYCLES,i as DEFAULT_MAX_PANES_PER_WINDOW,s as DEFAULT_WHIP_MESSAGES,c as DEFAULT_WHIP_MESSAGE_MODE,_ as ExitCode,v as MARKETPLACE_MANIFEST_DIR,w as MARKETPLACE_MANIFEST_FILE,I as MOUSE_MODE_DEFAULTS,f as PLUGIN_MANIFEST_DIR,E as PLUGIN_MANIFEST_FILE,y as PROFILE_DIR,r as SCHEMA_VERSION,b as STATE_FILE,T as WHIP_MESSAGE_MODES,d as defaultKindsConfig,u as defaultModelLaddersConfig,p as defaultRemoteCanvasConfig,D as defaultScopeConfig,M as defaultScopeState};
@@ -0,0 +1,54 @@
1
+ # `@north-light/crouter-sdk`
2
+
3
+ The client an application installs to drive a crouter daemon: create agent runs, wait for typed results, and reach the rest of the daemon's `/v1` API. Streaming and memory are planned phase-2 and phase-3 surfaces, not client methods today.
4
+
5
+ One package, one class. `new Crouter()` in Node talks to the owner's unix socket. `new Crouter({ baseURL, token })` in a browser or a remote process talks to the daemon's TCP listener with a bearer token. Resource methods issue `/v1` requests; `createAndWait`, `parse`, and `auth.status` compose more than one request. crtrd stays the sole owner of canvas state.
6
+
7
+ ```ts
8
+ import Crouter from '@north-light/crouter-sdk';
9
+ import { z } from 'zod';
10
+
11
+ const client = new Crouter();
12
+
13
+ const run = await client.nodes.parse({
14
+ prompt: 'Summarize the failing tests in this repo.',
15
+ cwd: '/path/to/repo',
16
+ output_schema: z.object({ failures: z.array(z.string()), root_cause: z.string() }),
17
+ });
18
+
19
+ if (run.kind === 'result') console.log(run.output_parsed.root_cause);
20
+ ```
21
+
22
+ ## Pages
23
+
24
+ | Page | Covers |
25
+ |---|---|
26
+ | [Getting started](./getting-started.md) | Install, `new Crouter()` on the owner's socket, `client.auth.status()` and `crtr sys connect` for a browser or remote app, Chrome's local-network prompt |
27
+ | [Client construction](./client.md) | Every constructor option and its environment-variable fallback; per-request options |
28
+ | [Nodes](./nodes.md) | `create` parameters, the outcome union, `parse()` with a zod schema, `waitForOutcome`, `message`, `cancel`, nested resources |
29
+ | [Streaming](./streaming.md) | The event table, `stream()` / `events()`, resume with `after`, `stream_gap` and `stream_dropped` — **phase 2** |
30
+ | [Memory](./memory.md) | The scope object and every `client.memory` method — **phase 3** |
31
+ | [Resource map](./resources.md) | Every namespace with its phase, what is deliberately excluded, and the `client.request()` escape hatch |
32
+ | [Errors](./errors.md) | The error class table and the retry policy |
33
+ | [Docker environment](./docker.md) | `start()`, `attach()`, `connection()` from `@north-light/crouter-env-docker` |
34
+ | [Migration](./migration.md) | Moving off `generate()` and `local()` — a hard cut, with before/after |
35
+
36
+ ## Phases
37
+
38
+ The surface ships in three cuts. Every page marks which cut a method belongs to; a method marked phase 2 or phase 3 does not exist yet.
39
+
40
+ | Phase | What lands | State |
41
+ |---|---|---|
42
+ | **Phase 1** | Client construction and transports; `client.auth.status()`; `client.nodes.create`, `retrieve`, `list`, `outcome`, `waitForOutcome`, `createAndWait`, `parse`, `message`, `cancel`, and `interrupt`; `nodes.reports.list`; `profiles.ensure` and `retrieve`; `system.status` and `health`; `files.peek`; the error hierarchy; per-request options; `crtr sys connect`; and `env-docker.connection()` | Shipped |
43
+ | **Phase 2** | Streaming (`GET /v1/nodes/{id}/events`, `nodes.stream`, `nodes.events`), plus the canvas, cron, human-inbox, and model namespaces | Not shipped |
44
+ | **Phase 3** | Memory routes and `client.memory`, plus the review, comment, and chat-inventory namespaces | Not shipped |
45
+
46
+ The approved per-run `scopes` field and `nodes.update` scope support are not in the shipped client or wire declarations, so this reference does not document them as phase 1.
47
+
48
+ ## Verification status
49
+
50
+ Phase-1 pages describe the shipped declarations. The remaining verification markers are confined to the phase-2 streaming and phase-3 memory pages.
51
+
52
+ ## Where the agents read this
53
+
54
+ The same content routed for agents lives in the builtin memory document `crouter-sdk`, which ships with the `crtr` binary. An agent working in an application's repository reaches it with `crtr memory read crouter-sdk` and does not need this repository checked out.
@@ -0,0 +1,92 @@
1
+ # Client construction
2
+
3
+ Phase 1.
4
+
5
+ ```ts
6
+ import Crouter from '@north-light/crouter-sdk';
7
+
8
+ const localClient = new Crouter(); // owner's own daemon, unix socket
9
+ const remoteClient = new Crouter({ baseURL: 'http://localhost:8787', token: 'token' }); // TCP with a bearer token
10
+ const customSocketClient = new Crouter({ socketPath: '/custom/path/crtrd.sock' }); // an explicit socket
11
+ ```
12
+
13
+ `Crouter` is the default export and the only class an application constructs. `CrtrClient` from `@north-light/crouter-api` is an implementation detail and is not re-exported.
14
+
15
+ ## Options
16
+
17
+ | Option | Type | Default | Meaning |
18
+ |---|---|---|---|
19
+ | `baseURL` | `string` | `CRTR_BASE_URL`, else unset | `http(s)://host:port` of a daemon TCP listener. |
20
+ | `socketPath` | `string` | `CRTR_SOCKET`, else `${CRTR_HOME}/crtrd.sock`, else `~/.crouter/canvas/crtrd.sock` | Unix socket. Node only; throws in a browser. |
21
+ | `token` | `string` | `CRTRD_TOKEN` | Sent as `Authorization: Bearer <token>`. Ignored by a unix-socket daemon, which authenticates by filesystem permission. |
22
+ | `timeout` | `number` (ms) | `30_000` | Per-request wall clock. Does not apply to a stream. |
23
+ | `maxRetries` | `number` | `2` | Transient-failure retries. Never applied to `POST` or `PATCH` — see [Errors](./errors.md). |
24
+ | `defaultHeaders` | `Record<string, string>` | `{}` | Merged into every request. |
25
+ | `headers` | `Record<string, string>` | unset | Merged after `defaultHeaders`; a matching name wins. |
26
+ | `fetch` | `typeof fetch` | global `fetch`, or the socket implementation when `socketPath` is used | Transport override, for proxies and instrumentation. |
27
+ | `autostart` | `boolean` | `true` for a socket, `false` for `baseURL` | On a cold socket, run `crtr sys daemon start` and retry once. Node only. |
28
+
29
+ ### Environment-variable fallbacks
30
+
31
+ Every fallback above is read at construction, not at request time.
32
+
33
+ | Variable | Fills |
34
+ |---|---|
35
+ | `CRTR_BASE_URL` | `baseURL` |
36
+ | `CRTR_SOCKET` | `socketPath` |
37
+ | `CRTR_HOME` | the directory the default socket path is resolved under (`$CRTR_HOME/crtrd.sock`) |
38
+ | `CRTRD_TOKEN` | `token` |
39
+
40
+ An explicit option always beats its environment variable.
41
+
42
+ ## Transport selection
43
+
44
+ `baseURL` wins if it is set; otherwise `socketPath`; otherwise the default socket path. **Passing both `baseURL` and `socketPath` throws `TypeError` at construction** — exactly one transport per client. In a browser, `new Crouter({})` has no daemon transport; give it the saved `baseURL` and token before making requests.
45
+
46
+ There is one request path. The client issues every request through the Web `fetch` API, which a browser and Node both supply globally. The unix socket is not a second transport — it is a `fetch` implementation the SDK installs when `socketPath` is set, built on `node:http` with `{ socketPath }`, returning a standard `Response` whose `body` is a `ReadableStream`. Streaming, abort, headers, and error parsing therefore have exactly one implementation, and the browser build never sees Node code.
47
+
48
+ ## Autostart
49
+
50
+ With `autostart` on (the default for a socket), a request that finds a cold socket runs `crtr sys daemon start`, waits for the daemon to serve, and retries the request once. This is what makes `npm i -g @north-light/crouter` followed by `new Crouter()` work on a machine that has never run the daemon.
51
+
52
+ Autostart is Node-only and applies only to the socket transport. A `baseURL` client cannot start a daemon it may not even share a machine with, so `autostart` defaults to `false` there; setting it to `true` on a `baseURL` client throws `CrouterError` (`autostart is only valid for the local socket transport`).
53
+
54
+ ## Per-request options
55
+
56
+ Every request-capable method accepts an options object as its last argument, after any path, body, or query arguments. Each field overrides the client-level default for that one request.
57
+
58
+ ```ts
59
+ const id = 'example-node';
60
+ const signal = new AbortController().signal;
61
+
62
+ await client.nodes.retrieve(id, {
63
+ signal, // AbortSignal — real cancellation, passed straight to fetch
64
+ timeout: 5_000, // ms, this request only
65
+ maxRetries: 0, // this request only
66
+ headers: { 'x-trace': 't-9' } // merged over defaultHeaders
67
+ });
68
+ ```
69
+
70
+ | Field | Type | Effect |
71
+ |---|---|---|
72
+ | `signal` | `AbortSignal` | Aborts the underlying `fetch`. Raises `APIUserAbortError`. |
73
+ | `timeout` | `number` (ms) | Overrides the client `timeout`. |
74
+ | `maxRetries` | `number` | Overrides the client `maxRetries`. |
75
+ | `headers` | `Record<string, string>` | Merged over `defaultHeaders` and the client's headers; a matching name, including `Authorization`, wins. |
76
+
77
+ `signal` is real cancellation. The long-polling helpers (`waitForOutcome`, `createAndWait`, `parse`) honour it between polls and during the in-flight request, and aborting them stops the client — it does not cancel the node. To stop the run itself, call `client.nodes.cancel(id)` or `client.nodes.interrupt(id)`.
78
+
79
+ ## What is not on the client
80
+
81
+ | Convention | Why it is absent |
82
+ |---|---|
83
+ | `.withResponse()` / `.asResponse()` | They exist to hand back the raw `Response` and an `x-request-id`. The daemon emits no request id, and a caller who needs raw bytes uses `client.request()`. |
84
+ | `local()` and the `Environment` interface | Deleted. The client *is* the connection — `new Crouter()` covers what `local()` did. See [Migration](./migration.md). |
85
+ | `apiKey` | The daemon's credential is a token (`CRTRD_TOKEN`) everywhere in the product. `apiKey` would be a second name for one thing. |
86
+ | Resource-prefixed ids | Node ids have their own format and are validated by the daemon. Nothing is re-prefixed. |
87
+
88
+ ## Wire naming
89
+
90
+ Wire fields are `snake_case` — `output_schema`, `root_lifecycle`, `pin_cwd`, `final_report_path`. The SDK does not camelize them. Namespaces and method names are camelCase (`client.nodes.waitForOutcome`, `client.nodes.outcomeDelivery`).
91
+
92
+ Timestamps are ISO-8601 strings (`created`, `settled_at`, `finalized_at`, `deadline_at`), not Unix seconds.
@@ -0,0 +1,63 @@
1
+ # Docker environment
2
+
3
+ Phase 1.
4
+
5
+ `@north-light/crouter-env-docker` runs a crouter daemon in a container and tells you how to reach it. It is a separate package because container lifecycle is real work the SDK does not otherwise do — and it **has no dependencies**, including on the SDK itself.
6
+
7
+ ```bash
8
+ npm i @north-light/crouter-env-docker
9
+ ```
10
+
11
+ ## Starting a container and connecting to it
12
+
13
+ ```ts
14
+ import { start } from '@north-light/crouter-env-docker';
15
+ import Crouter from '@north-light/crouter-sdk';
16
+
17
+ const env = await start({ volume: 'my-agent-home' });
18
+ const client = new Crouter(env.connection());
19
+
20
+ await client.nodes.create({ prompt: 'Do the thing.', root: true });
21
+
22
+ await env.stop();
23
+ ```
24
+
25
+ `connection()` returns `{ baseURL, headers }` — exactly the shape the `Crouter` constructor takes, so the result passes straight through with no adapter.
26
+
27
+ ## Methods
28
+
29
+ | Method | What it does |
30
+ |---|---|
31
+ | `start(opts?)` | Starts a container running `crtrd` and returns an environment handle. |
32
+ | `attach(name)` | Returns a handle for the already-running container named `name`. |
33
+ | `stop()` | On a `start()` handle, stops and removes the container. On an `attach()` handle, stops it without removing it. |
34
+ | `connection()` | `Connection` (`{ baseURL: string; headers?: Record<string, string> }`) — returned synchronously and passed to `new Crouter()`. |
35
+
36
+ | `start()` option | Type | Default | Effect |
37
+ |---|---|---|---|
38
+ | `image` | `string` | `ghcr.io/vallum-security/crtrd:latest` | Image to run. |
39
+ | `env` | `Record<string, string>` | unset | Extra container environment variables; they are not logged. |
40
+ | `name` | `string` | generated name | Container name for later `attach(name)`. |
41
+ | `volume` | `string` | unset | Named volume mounted at `/home/agent/.crouter`. |
42
+ | `port` | `number` | Docker-assigned port | Host port bound on `127.0.0.1`. |
43
+
44
+ ## `connection()` replaced `daemon()`
45
+
46
+ `daemon()` is gone, along with the package's `Environment` type. The new name says what the method returns, and its field is spelled `baseURL` to match the constructor option rather than `baseUrl`.
47
+
48
+ ```text
49
+ // before
50
+ const env = await start({ volume: 'my-agent-home' });
51
+ const { baseUrl, headers } = await env.daemon();
52
+ const client = new CrtrClient({ baseUrl, headers });
53
+
54
+ // after
55
+ const env = await start({ volume: 'my-agent-home' });
56
+ const client = new Crouter(env.connection());
57
+ ```
58
+
59
+ `connection()` is typed structurally — `{ baseURL: string; headers?: Record<string, string> }` — which is how the package stays dependency-free while producing something the SDK accepts directly.
60
+
61
+ ## The volume is the agent's home
62
+
63
+ The `volume` you pass is the container's canvas home: nodes, transcripts, artifacts, memory, and profiles all live there. Reuse the same volume across `start` calls and the agent keeps everything it learned; use a fresh one and it starts from nothing. `start()` mounts an existing named volume unchanged; it does not inspect or reject it based on the crouter version that last wrote it.
@@ -0,0 +1,85 @@
1
+ # Errors
2
+
3
+ Phase 1.
4
+
5
+ ## What throws and what does not
6
+
7
+ **An agent's own outcome is returned, never thrown.** A run that declined the schema, hit its deadline, or crashed comes back as a settled `NodeOutcome` from `waitForOutcome`, `createAndWait`, or `parse`. Narrow on it — see [Nodes](./nodes.md#outcomes).
8
+
9
+ Exceptions are for the layer underneath: the daemon refused the request, the connection failed, or you aborted.
10
+
11
+ ## Classes
12
+
13
+ Every class extends `APIError`, which carries `status`, `code`, `message`, `details`, and `headers`.
14
+
15
+ | Condition | Class |
16
+ |---|---|
17
+ | HTTP 400 | `BadRequestError` |
18
+ | HTTP 401 | `AuthenticationError` |
19
+ | HTTP 403 | `PermissionDeniedError` |
20
+ | HTTP 404 | `NotFoundError` |
21
+ | HTTP 409 | `ConflictError` |
22
+ | HTTP 413, 422 | `UnprocessableEntityError` |
23
+ | HTTP 429 | `RateLimitError` |
24
+ | HTTP ≥ 500 | `InternalServerError` |
25
+ | Transport failure | `APIConnectionError` |
26
+ | Request timeout | `APIConnectionTimeoutError` (extends `APIConnectionError`) |
27
+ | Caller abort | `APIUserAbortError` |
28
+ | Anything else | `APIError` |
29
+
30
+ ```ts
31
+ import { ConflictError, NotFoundError, APIConnectionError } from '@north-light/crouter-sdk';
32
+
33
+ try {
34
+ await client.nodes.create({ prompt, node_id: 'my-run-42' });
35
+ } catch (e) {
36
+ if (e instanceof ConflictError && e.code === 'node_id_exists') {
37
+ // the run already exists — attach to it instead of spawning a second one
38
+ return client.nodes.waitForOutcome('my-run-42');
39
+ }
40
+ if (e instanceof NotFoundError) throw new Error('no such node');
41
+ if (e instanceof APIConnectionError) throw new Error('the daemon is not reachable');
42
+ throw e;
43
+ }
44
+ ```
45
+
46
+ The subclasses add **no fields**. `status` and `code` on the base class already decide everything there is to branch on. They exist so that `catch (e) { if (e instanceof NotFoundError) … }` — what an OpenAI SDK user writes without thinking about it — works here too.
47
+
48
+ ## The wire body
49
+
50
+ The daemon answers an error with:
51
+
52
+ ```json
53
+ { "error": { "code": "node_id_exists", "message": "…", "details": { } } }
54
+ ```
55
+
56
+ `code` is the stable, machine-readable identity — `node_id_exists`, `daemon_unavailable`, `request_timeout`, `node_dormant`. Branch on it. There is no separate `type` field and no `param` field.
57
+
58
+ ## Retries
59
+
60
+ `maxRetries` defaults to `2`, with exponential backoff. It applies to:
61
+
62
+ - connection errors, and
63
+ - HTTP 429 and 5xx responses, **on `GET`, `HEAD`, and `DELETE` only**.
64
+
65
+ **`POST` and `PATCH` are never retried automatically.** Creating a node, delivering a message, and pushing a report are not idempotent, and a mutation whose response was interrupted may already have been applied — replaying it would spawn a second agent, deliver a second message, or push a second report.
66
+
67
+ If you want a create that survives a retry, pass `node_id`. A duplicate then fails loudly with `409 node_id_exists` instead of quietly running twice:
68
+
69
+ ```ts
70
+ const runId = `invoice-${invoice.id}`;
71
+
72
+ try {
73
+ await client.nodes.create({ prompt, node_id: runId, root: true });
74
+ } catch (e) {
75
+ if (!(e instanceof ConflictError && e.code === 'node_id_exists')) throw e;
76
+ }
77
+
78
+ const outcome = await client.nodes.waitForOutcome(runId);
79
+ ```
80
+
81
+ Override the policy per request with `{ maxRetries: 0 }` or `{ maxRetries: 5 }`. Raising it on a `POST` still does not make the client retry that `POST`.
82
+
83
+ ## In-repo callers
84
+
85
+ `ApiError` from `@north-light/crouter-api` is an alias of `APIError`, so existing crouter code and its transport checks keep working unchanged. Daemon handlers keep throwing `{ status, code, message }` and never name an SDK class — the hierarchy is one `status → constructor` table on the client side and nothing else depends on it.