pi-daddy 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/PRODUCT-GUIDE.md +125 -0
- package/README.md +16 -1
- package/REQUIREMENTS.md +41 -0
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +35 -7
- package/dist/cli.js.map +1 -1
- package/dist/daily-dashboard-host.d.ts +14 -1
- package/dist/daily-dashboard-host.d.ts.map +1 -1
- package/dist/daily-dashboard-host.js +40 -17
- package/dist/daily-dashboard-host.js.map +1 -1
- package/dist/daily-panel.d.ts +36 -0
- package/dist/daily-panel.d.ts.map +1 -0
- package/dist/daily-panel.js +84 -0
- package/dist/daily-panel.js.map +1 -0
- package/dist/daily-view-render.d.ts +2 -1
- package/dist/daily-view-render.d.ts.map +1 -1
- package/dist/daily-view-render.js +2 -1
- package/dist/daily-view-render.js.map +1 -1
- package/dist/daily-view.d.ts +3 -0
- package/dist/daily-view.d.ts.map +1 -1
- package/dist/daily-view.js +6 -0
- package/dist/daily-view.js.map +1 -1
- package/dist/dashboard-cli.d.ts +2 -0
- package/dist/dashboard-cli.d.ts.map +1 -1
- package/dist/dashboard-cli.js +37 -15
- package/dist/dashboard-cli.js.map +1 -1
- package/dist/dashboard-host-contract.d.ts +2 -0
- package/dist/dashboard-host-contract.d.ts.map +1 -1
- package/dist/dashboard-host-contract.js.map +1 -1
- package/dist/dashboard-host-transport.d.ts.map +1 -1
- package/dist/dashboard-host-transport.js +6 -3
- package/dist/dashboard-host-transport.js.map +1 -1
- package/dist/dashboard-host.d.ts +30 -2
- package/dist/dashboard-host.d.ts.map +1 -1
- package/dist/dashboard-host.js +73 -11
- package/dist/dashboard-host.js.map +1 -1
- package/dist/dashboard-menu.d.ts +16 -0
- package/dist/dashboard-menu.d.ts.map +1 -0
- package/dist/dashboard-menu.js +33 -0
- package/dist/dashboard-menu.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/learning-connection.d.ts +127 -0
- package/dist/learning-connection.d.ts.map +1 -0
- package/dist/learning-connection.js +54 -0
- package/dist/learning-connection.js.map +1 -0
- package/dist/model-preflight.d.ts +5 -0
- package/dist/model-preflight.d.ts.map +1 -1
- package/dist/model-preflight.js +9 -0
- package/dist/model-preflight.js.map +1 -1
- package/dist/product-files.d.ts +6 -0
- package/dist/product-files.d.ts.map +1 -0
- package/dist/product-files.js +55 -0
- package/dist/product-files.js.map +1 -0
- package/dist/work-command.d.ts.map +1 -1
- package/dist/work-command.js +14 -6
- package/dist/work-command.js.map +1 -1
- package/dist/work-policy-registry.d.ts +133 -0
- package/dist/work-policy-registry.d.ts.map +1 -0
- package/dist/work-policy-registry.js +128 -0
- package/dist/work-policy-registry.js.map +1 -0
- package/dist/work-run.d.ts +56 -0
- package/dist/work-run.d.ts.map +1 -0
- package/dist/work-run.js +102 -0
- package/dist/work-run.js.map +1 -0
- package/dist/work-setup.d.ts +40 -0
- package/dist/work-setup.d.ts.map +1 -0
- package/dist/work-setup.js +174 -0
- package/dist/work-setup.js.map +1 -0
- package/extensions/daily-dashboard-session.ts +16 -7
- package/extensions/execute-child.ts +2 -2
- package/extensions/grants-command.ts +2 -2
- package/extensions/grants.ts +6 -2
- package/extensions/learning-outcome-session.ts +38 -0
- package/extensions/learning-session.ts +59 -0
- package/extensions/ordinary-runtime.ts +2 -2
- package/extensions/run-delegation.ts +2 -2
- package/extensions/work-policy-session.ts +139 -0
- package/extensions/work-run-session.ts +75 -0
- package/extensions/work-runtime.ts +2 -2
- package/extensions/work-session.ts +132 -0
- package/package.json +9 -1
- package/src/cli.ts +26 -7
- package/src/daily-dashboard-host.ts +32 -15
- package/src/daily-panel.ts +80 -0
- package/src/daily-view-render.ts +2 -1
- package/src/daily-view.ts +8 -0
- package/src/dashboard-cli.ts +29 -17
- package/src/dashboard-host-contract.ts +2 -0
- package/src/dashboard-host-transport.ts +7 -4
- package/src/dashboard-host.ts +48 -13
- package/src/dashboard-menu.ts +28 -0
- package/src/index.ts +7 -1
- package/src/learning-connection.ts +70 -0
- package/src/model-preflight.ts +9 -0
- package/src/product-files.ts +34 -0
- package/src/work-command.ts +11 -6
- package/src/work-policy-registry.ts +81 -0
- package/src/work-run.ts +79 -0
- package/src/work-setup.ts +141 -0
|
@@ -14,12 +14,16 @@ import { bindWorkIntent } from "./intent-application.ts";
|
|
|
14
14
|
import { intentKey, intentRequest, intentRequestDigest, type IntentPriority, type IntentRequest, type IntentSelection } from "./intent-control.ts";
|
|
15
15
|
import { parseWorkLedgerText, type WorkRevisionEvent, type WorkSnapshotEvent } from "./work-ledger.ts";
|
|
16
16
|
import { resolveWorkSnapshotText } from "./work-ledger-snapshot.ts";
|
|
17
|
+
import { workPresentation, workSetupForSelection } from "./work-setup.ts";
|
|
18
|
+
import { createDailyViewReader } from "./daily-view.ts";
|
|
19
|
+
import { learningHarness, learningScopeDigest, type LearningConnection } from "./learning-connection.ts";
|
|
17
20
|
|
|
18
21
|
export interface DailyIntentAction {key:string;label:string;action:"revise-scope"|"reprioritize"|"select-alternative";fromSelection:IntentSelection;events:(WorkRevisionEvent|WorkSnapshotEvent)[];selection:IntentSelection;priorities:IntentPriority[]}
|
|
19
|
-
export interface DailyDashboardHostInput {id:string;cwd:string;directory:string;socketPath?:string;declared:DeclaredWorkState;ordinary:OrdinaryChildren;harness:DashboardHarness;author:string;priorities?:IntentPriority[];intentActions?:DailyIntentAction[];presence?:()=>{present:boolean;closing:boolean;evidenceDigest:string;expiresAt:number}|null;onDeclaredWorkChanged?:(state:DeclaredWorkState)=>void|Promise<void>}
|
|
22
|
+
export interface DailyDashboardHostInput {id:string;cwd:string;directory:string;socketPath?:string;declared:DeclaredWorkState;ordinary:OrdinaryChildren;harness:DashboardHarness;author:string;learning?:LearningConnection;priorities?:IntentPriority[];intentActions?:DailyIntentAction[];presence?:()=>{present:boolean;closing:boolean;evidenceDigest:string;expiresAt:number}|null;onDeclaredWorkChanged?:(state:DeclaredWorkState)=>void|Promise<void>}
|
|
20
23
|
const idPattern=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
21
24
|
const hash=(value:string)=>createHash("sha256").update(value).digest("hex");
|
|
22
25
|
const same=(a:unknown,b:unknown)=>intentKey(a)===intentKey(b);
|
|
26
|
+
const friendlyLabel=(value:string)=>{let chars=[...value.replace(/[\p{Cc}\p{Cf}]/gu," ")];while(Buffer.byteLength(chars.join(""))>112)chars.pop();return chars.join("")+(chars.length<[...value].length?"...":"");};
|
|
23
27
|
/** Recorded P01 snapshots become labelled proposals; choosing a label still traverses intent validation and exact host/CAS authority. */
|
|
24
28
|
export function discoverDailyIntentActions(text:string,selection:IntentSelection,priorities:readonly IntentPriority[]):DailyIntentAction[]{
|
|
25
29
|
const parsed=parseWorkLedgerText(text);if(!parsed.complete||parsed.events.length>512)throw Error("bounded complete recorded proposal ledger required");const label=(value:string)=>value.length<=80?value:`${value.slice(0,67)}…${hash(value).slice(0,12)}`;
|
|
@@ -41,48 +45,61 @@ export async function startDailyDashboardHost(input:DailyDashboardHostInput){
|
|
|
41
45
|
if(!idPattern.test(input.id)||!isAbsolute(input.cwd)||resolve(input.cwd)!==input.cwd||!isAbsolute(input.directory)||resolve(input.directory)!==input.directory||!input.author.trim()||input.author.length>256)throw Error("explicit bounded daily host configuration required");
|
|
42
46
|
const harnessDigest=loadedDashboardHarnessDigest(input.harness);if(!harnessDigest)throw Error("loaded skill-harness bridge required");
|
|
43
47
|
if(input.declared.ledgerPath!==join(input.cwd,".pi","work.jsonl"))throw Error("current declared work must belong to this host project");
|
|
44
|
-
|
|
48
|
+
const learning=input.learning?learningHarness(input.harness).openLearningWorkspace(input.learning.directory):null,learningConfig=learning?.configuration();
|
|
49
|
+
if(input.learning&&(!learningConfig||intentKey(learningConfig)!==intentKey(input.learning.configuration)||learningConfig.scopeDigest!==learningScopeDigest(input.declared)||learningConfig.author!==input.author||intentKey(input.learning.selection)!==intentKey(input.declared.selectedSnapshot)))throw Error("learning scope/archive/population/author mismatch");
|
|
50
|
+
const learnedTrust=learning?.dashboardBinding();
|
|
51
|
+
await mkdir(input.directory,{mode:0o700});const archiveRoot=learningConfig?.archiveRoot??join(input.directory,"archive");if(!learningConfig)await mkdir(archiveRoot,{mode:0o700});
|
|
45
52
|
const reference=input.harness.retainArchiveSource(archiveRoot,{sourceId:`host-${input.id}-bootstrap`,parser:{id:"daily-host-bootstrap",version:"1"},retention:"exact",bytes:Buffer.from("explicit daily host bootstrap")});
|
|
46
|
-
const component={kind:"detector",id:"coverage_gap",version:"work-signals-v1:declared-work-v1",population
|
|
53
|
+
const component={kind:"detector",id:"coverage_gap",version:"work-signals-v1:declared-work-v1",population:learningConfig?.population??`daily:${input.id}`};
|
|
47
54
|
const trustInput={archiveRoot,component,seed:hash(`daily-host\0${input.id}`),maxUnflagged:0,cohort:[{incidentId:"bootstrap",manifestId:reference.manifestId,flagged:true,split:"calibration"}],exposure:null};
|
|
48
|
-
const trustPolicyId=input.harness.trustPolicyDigest(trustInput),trustDirectory=join(input.directory,"trust");
|
|
55
|
+
const trustPolicyId=learnedTrust?.trustPolicyId??input.harness.trustPolicyDigest(trustInput),trustDirectory=learnedTrust?.trustDirectory??join(input.directory,"trust");
|
|
56
|
+
if(Boolean(learnedTrust?.trustDirectory)!==Boolean(learnedTrust?.trustPolicyId))throw Error("incomplete original learning trust binding");
|
|
57
|
+
if(!learnedTrust?.trustDirectory)input.harness.createTrustLifecycle(trustDirectory,trustInput,[trustPolicyId]);
|
|
49
58
|
const workRelative="work.jsonl",factsRelative="facts.jsonl",workMirror=join(input.directory,workRelative);await writeFile(workMirror,await readFile(input.declared.ledgerPath),{mode:0o600,flag:"wx"});
|
|
50
59
|
const refreshWork=async()=>{const temporary=`${workMirror}.${process.pid}.${randomUUID()}.tmp`;try{await writeFile(temporary,await readFile(input.declared.ledgerPath),{mode:0o600,flag:"wx"});await rename(temporary,workMirror);}finally{await rm(temporary,{force:true});}};
|
|
51
|
-
const facts={scopeDigest:input.declared.selectedSnapshot.snapshot.digest,version:"declared-work-v1",population
|
|
60
|
+
const facts={scopeDigest:input.declared.selectedSnapshot.snapshot.digest,version:"declared-work-v1",population:learningConfig?.population??`daily:${input.id}`,expectedWaits:[],checkpoints:[],violations:[],priorAccepted:[]};await writeFile(join(input.directory,factsRelative),JSON.stringify(facts)+"\n",{mode:0o600,flag:"wx"});
|
|
52
61
|
const policy={version:"archive-policy-v2",id:`daily-${input.id}`,revision:"1",sourceRoot:input.directory,archiveRoot,maxBytes:8*1024*1024,retention:"exact",expiresAt:new Date(Date.now()+24*60*60*1000).toISOString(),sources:[{id:"work",path:workRelative,parser:{id:"pi-daddy-work-ledger",version:"4"},contentPolicy:"manifest-only"},{id:"facts",path:factsRelative,parser:{id:"pi-daddy-work-signal-facts",version:"1"},contentPolicy:"manifest-only"}]};
|
|
53
62
|
const policyPath=join(input.directory,"archive-policy.json"),policyBytes=JSON.stringify(policy);await writeFile(policyPath,policyBytes,{mode:0o600,flag:"wx"});
|
|
54
|
-
const authorityDigest=dataDigest({kind:"daily-dashboard-host",id:input.id,cwd:input.cwd}),initialPriorities=input.priorities??
|
|
63
|
+
const authorityDigest=dataDigest({kind:"daily-dashboard-host",id:input.id,cwd:input.cwd}),initialPriorities=input.priorities??resolveWorkSnapshotText(await readFile(input.declared.ledgerPath,"utf8"),input.declared.selectedSnapshot).snapshot!.bindings.map((binding,rank)=>({obligation:binding.obligation,rank}));
|
|
55
64
|
const workIntent=await bindWorkIntent({path:input.declared.ledgerPath,grantLedgerPath:input.declared.grantLedgerPath,selection:input.declared.selectedSnapshot,priorities:initialPriorities});
|
|
56
65
|
const budget=await createIntentBudget({directory:join(input.directory,"budget"),authorityDigest,limits:{maxAttempts:6,maxInputBytes:65536,maxConcurrent:3}},workIntent);
|
|
57
|
-
const config:DashboardHostConfig={version:"producer-dashboard-host-v1",trustDirectory,trustPolicyId,archiveRoot,scope:`daily:${input.id}`,author:input.author,policyPath,policySha256:hash(policyBytes),sources:[{id:"work",kind:"work"},{id:"facts",kind:"facts"}],selection:input.declared.selectedSnapshot,cases:null,blind:null,budgetDigest:resourceBindingDigest(budget),experimentDigest:null,harnessArtifactDigest:harnessDigest,ordinaryDigest:input.ordinary.bindingDigest};
|
|
66
|
+
const config:DashboardHostConfig={version:"producer-dashboard-host-v1",...(learnedTrust?.trustDirectory?{journalDirectory:join(input.directory,"producer-host")} : {}),trustDirectory,trustPolicyId,archiveRoot,scope:`daily:${input.id}`,author:input.author,policyPath,policySha256:hash(policyBytes),sources:[{id:"work",kind:"work"},{id:"facts",kind:"facts"}],selection:input.declared.selectedSnapshot,cases:null,blind:null,budgetDigest:resourceBindingDigest(budget),experimentDigest:null,harnessArtifactDigest:harnessDigest,ordinaryDigest:input.ordinary.bindingDigest};
|
|
58
67
|
const proposals=input.intentActions??[];if(proposals.length>8||new Set(proposals.map(x=>x.key)).size!==proposals.length||proposals.some(x=>!idPattern.test(x.key)||typeof x.label!=="string"||!x.label.trim()||Buffer.byteLength(x.label)>120))throw Error("bounded friendly intent actions required");
|
|
59
68
|
const requestDigests=new Set<string>(),dispatchDigests=new Set<string>(),ordinaryDigests=new Set<string>();let host:ReturnType<typeof createDashboardHost>,factsResultManifest:string|null=null,workSelection:IntentSelection=input.declared.selectedSnapshot,declaredState=input.declared,reboundSnapshotId:string|null=null;
|
|
60
69
|
const authority=():DashboardHostAuthority=>({hostDigests:[dashboardHostDigest(config)],requestDigests:[...requestDigests],workContext:{selectedSnapshot:workSelection,authority:null},dispatch:{authorityDigest,requestDigests:[...dispatchDigests]},experiment:null,ordinary:{bindingDigest:input.ordinary.bindingDigest,requestDigests:[...ordinaryDigests]}});
|
|
61
70
|
const request=(context:{hostDigest:string;selectionDigest:string;tip:string},operation:DashboardHostRequest["operation"],payload:unknown,requestId:string):DashboardHostRequest=>({version:"1.0",requestId,hostDigest:context.hostDigest,selectionDigest:context.selectionDigest,expectedTip:context.tip,operation,payload});
|
|
62
|
-
|
|
71
|
+
const dailyReader=createDailyViewReader();
|
|
72
|
+
host=createDashboardHost({harness:input.harness,config,budget,ordinary:input.ordinary,authority,learningView:learning?()=>learningScopeDigest(declaredState)===learningConfig!.scopeDigest?learning.inspect(Date.now()):{state:"deferred",reason:"Selected work changed; reconnect learning explicitly"}:undefined,afterObservation:learning?observation=>{if(observation.cases&&learningScopeDigest(declaredState)===learningConfig!.scopeDigest)learning.bindCases({name:`work-${observation.cases.batchId.slice(0,20)}`,title:"Retained daily work",version:3,batchId:observation.cases.batchId});}:undefined,presentation:()=>workPresentation(declaredState),currentWork:()=>dailyReader({workLedgerPath:declaredState.ledgerPath,workContext:{selectedSnapshot:workSelection,authority:null}}),beforeObservation:sourceId=>sourceId==="work"?refreshWork():Promise.resolve(),humanActions:async context=>{
|
|
63
73
|
const state=await openResourceBudget(budget).controls(null).inspect(),action=state.paused?"resume-dispatch":"pause-dispatch",key=state.paused?"resume-dispatch":"pause-new-dispatch";
|
|
64
74
|
const native:DispatchRequest={version:"1.0",requestId:`${action}:${state.revision}`,bindingDigest:config.budgetDigest,expectedRevision:state.revision,action,targetExecutionId:null};dispatchDigests.add(dispatchRequestDigest(native));
|
|
65
75
|
const hostRequest=request(context,"dispatch",native,`host-${action}:${state.revision}`);requestDigests.add(dashboardHostRequestDigest(hostRequest));
|
|
66
76
|
const previous=context.observations.find(x=>x.sourceId==="work")?.checkpointId??null,refresh=request(context,"observe",{sourceId:"work",previousCheckpointId:previous,facts:context.observations.find(x=>x.sourceId==="facts")?.checkpointId?factsResultManifest:null},`refresh-work:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(refresh));
|
|
67
77
|
const intentState=await openResourceBudget(budget).intentControls(null).inspect(),automatic=discoverDailyIntentActions(await readFile(input.declared.ledgerPath,"utf8"),intentState.selection,intentState.priorities);
|
|
68
78
|
const candidates=[...proposals,...automatic].filter((p,index,all)=>all.findIndex(x=>x.key===p.key)===index).filter(p=>p.action==="reprioritize"||Boolean(input.onDeclaredWorkChanged)).slice(0,8);
|
|
69
|
-
const
|
|
79
|
+
const labels=await workPresentation(declaredState);
|
|
80
|
+
const intents=await Promise.all((input.ordinary.quiescent()?candidates:[]).filter(p=>intentKey(p.fromSelection)===intentKey(intentState.selection)).map(async p=>{
|
|
70
81
|
const native=intentRequest({version:"intent-request-v1",requestId:`${p.key}:${intentState.revision}`,bindingDigest:config.budgetDigest,expectedRevision:intentState.revision,expectedSelection:intentState.selection,action:p.action,events:p.events,selection:p.selection,priorities:p.priorities});dispatchDigests.add(intentRequestDigest(native));
|
|
71
|
-
const proposed=request(context,"intent",native,`host-intent:${p.key}:${intentState.revision}`);requestDigests.add(dashboardHostRequestDigest(proposed));
|
|
72
|
-
|
|
82
|
+
const proposed=request(context,"intent",native,`host-intent:${p.key}:${intentState.revision}`);requestDigests.add(dashboardHostRequestDigest(proposed));const outcome=p.action==="reprioritize"?labels?.obligations.find(o=>o.digest===p.priorities[0]?.obligation.digest)?.outcome:(await workSetupForSelection(input.cwd,p.selection))?.setup.outcome;
|
|
83
|
+
return {key:p.key,label:outcome?friendlyLabel(p.action==="reprioritize"?`Put ${outcome} first`:`Use ${outcome}`):p.label,request:proposed};
|
|
84
|
+
}));
|
|
73
85
|
const ordinary=input.ordinary.inspect() as {revision:number;children:{target:OrdinaryTarget;state:string;abortRequested:boolean}[]},accounting=await openResourceBudget(budget).inspect(),presence=input.presence?.();
|
|
74
86
|
const presentation:{key:string;label:string;request:DashboardHostRequest}[]=[];
|
|
75
87
|
if(context.preparedPresentationDigest){const shown=request(context,"presented",{frameDigest:context.preparedPresentationDigest},`host-presented:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(shown));presentation.push({key:"acknowledge-case-cards",label:"Acknowledge these case cards are visible",request:shown});}
|
|
76
|
-
else if(presence?.present===true&&presence.closing===true&&presence.expiresAt>Date.now()&&state.paused&&accounting.active===0&&!ordinary.children.some(row=>row.state==="active")){const present=request(context,"present",{userPresent:true,closing:true,evidenceDigest:presence.evidenceDigest,dispatchRevision:state.revision},`host-present:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(present));presentation.push({key:"prepare-case-cards",label:"Prepare up to five retained case cards",request:present});}
|
|
88
|
+
else if((!learningConfig||learningScopeDigest(declaredState)===learningConfig.scopeDigest)&&presence?.present===true&&presence.closing===true&&presence.expiresAt>Date.now()&&state.paused&&accounting.active===0&&!ordinary.children.some(row=>row.state==="active")){const present=request(context,"present",{userPresent:true,closing:true,evidenceDigest:presence.evidenceDigest,dispatchRevision:state.revision},`host-present:${state.revision}:${context.tip.slice(0,12)}`);requestDigests.add(dashboardHostRequestDigest(present));presentation.push({key:"prepare-case-cards",label:"Prepare up to five retained case cards",request:present});}
|
|
77
89
|
const cancellations=ordinary.children.filter(row=>row.state==="active"&&!row.abortRequested).sort((a,b)=>a.target.executionId.localeCompare(b.target.executionId)).slice(0,8).map(row=>{
|
|
78
90
|
const native=ordinaryCancellation({version:"ordinary-cancel-v1",requestId:`cancel:${row.target.executionId.slice(5)}`,bindingDigest:input.ordinary.bindingDigest,expectedRevision:ordinary.revision,target:row.target});ordinaryDigests.add(ordinaryCancellationDigest(native));
|
|
79
91
|
const cancel=request(context,"ordinary-cancel",native,`host-cancel:${row.target.executionId.slice(5)}:${ordinary.revision}`);requestDigests.add(dashboardHostRequestDigest(cancel));
|
|
80
|
-
return {key:`cancel-${row.target.executionId.replace(":","-")}-${ordinary.revision.toString(36)}`,label:`
|
|
92
|
+
return {key:`cancel-${row.target.executionId.replace(":","-")}-${ordinary.revision.toString(36)}`,label:`Stop active agent ${ordinary.children.filter(r=>r.state==="active").indexOf(row)+1}`,request:cancel};
|
|
81
93
|
});
|
|
82
|
-
return [{key,label:state.paused?"Resume new
|
|
94
|
+
return [{key,label:state.paused?"Resume new work":"Pause new work (active agents continue)",request:hostRequest},{key:"refresh-current-work",label:reboundSnapshotId?"Retain current work and attempts (new scope)":"Retain current work and attempts",request:refresh},...intents,...presentation,...cancellations];
|
|
83
95
|
},afterIntentApplied:async selection=>{const rebound=await rebindDeclaredWork(declaredState,selection);declaredState=rebound;workSelection=selection;reboundSnapshotId=selection.snapshot.id;await input.onDeclaredWorkChanged?.(rebound);},presence:input.presence});
|
|
84
96
|
const before=await host.frame(),observeFacts=request({hostDigest:host.hostDigest,selectionDigest:before.selectionDigest,tip:before.tip},"observe",{sourceId:"facts",previousCheckpointId:null,facts:null},"observe-current-facts");requestDigests.add(dashboardHostRequestDigest(observeFacts));const factsResult=await host.action(observeFacts) as {result:{sourceManifestId:string}};factsResultManifest=factsResult.result.sourceManifestId;
|
|
85
97
|
const afterFacts=await host.frame(),observeWork=request({hostDigest:host.hostDigest,selectionDigest:afterFacts.selectionDigest,tip:afterFacts.tip},"observe",{sourceId:"work",previousCheckpointId:null,facts:factsResult.result.sourceManifestId},"observe-current-work");requestDigests.add(dashboardHostRequestDigest(observeWork));await host.action(observeWork);
|
|
86
98
|
const socketPath=input.socketPath??join(input.directory,"dashboard.sock"),server=await serveDashboardHost(socketPath,host);
|
|
87
|
-
let closed=false;
|
|
99
|
+
let closed=false;const disconnect=async()=>{if(closed)return;closed=true;await server.close();};
|
|
100
|
+
return Object.freeze({host,socketPath,directory:input.directory,
|
|
101
|
+
async close(){if(closed)return;await host.stop();await disconnect();},
|
|
102
|
+
/** Final Pi owner-session disposal, never a manual host replacement or implicit resume. */
|
|
103
|
+
async endSession(){host.endSession();await disconnect();}
|
|
104
|
+
});
|
|
88
105
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DailyAttempt, DailyView } from "./daily-view.ts";
|
|
2
|
+
import { renderDailyDetails } from "./daily-view-render.ts";
|
|
3
|
+
|
|
4
|
+
export interface WorkPresentation {
|
|
5
|
+
outcome: string;
|
|
6
|
+
obligations: { digest: string; outcome: string; agent?: string; dependencies?: string[] }[];
|
|
7
|
+
}
|
|
8
|
+
export interface PanelAction { key: string; label: string; operation?: string; requestDigest?: string }
|
|
9
|
+
export interface PanelOptions {
|
|
10
|
+
details?: boolean;
|
|
11
|
+
presentation?: WorkPresentation | null;
|
|
12
|
+
connected?: boolean;
|
|
13
|
+
error?: string | null;
|
|
14
|
+
controls?: unknown;
|
|
15
|
+
control?: string;
|
|
16
|
+
actions?: readonly { choice: string; label: string }[];
|
|
17
|
+
learning?: string[];
|
|
18
|
+
}
|
|
19
|
+
export const panelText = (value: unknown): string => String(value ?? "unknown").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
20
|
+
/** Conservative terminal-cell bound: non-ASCII glyphs take two cells; no ANSI/control bytes survive. */
|
|
21
|
+
export function panelLines(lines: readonly string[], width = 80): string {
|
|
22
|
+
const columns = Number.isFinite(width) ? Math.max(12, Math.min(240, Math.floor(width))) : 80;
|
|
23
|
+
return lines.flatMap(line => {
|
|
24
|
+
let row = "", cells = 0; const rows: string[] = [];
|
|
25
|
+
for (const char of panelText(line)) {
|
|
26
|
+
const size = char.codePointAt(0)! > 127 ? 2 : 1;
|
|
27
|
+
if (cells + size > columns) { rows.push(row); row = ""; cells = 0; }
|
|
28
|
+
row += char; cells += size;
|
|
29
|
+
}
|
|
30
|
+
rows.push(row); return rows;
|
|
31
|
+
}).join("\n");
|
|
32
|
+
}
|
|
33
|
+
export function learningSummary(value: unknown): string[] {
|
|
34
|
+
if (!value || typeof value !== "object") return ["Not connected — /grants learning"];
|
|
35
|
+
const v=value as {state?:string;reason?:string;cases?:unknown[];comparisons?:{state?:string;reason?:string;title?:string}[];trust?:{automaticExposure?:{mode?:string;reason?:string};reason?:string}};
|
|
36
|
+
if(v.state==="deferred")return [`Deferred: ${v.reason??"evidence unavailable"}`,"/grants learning to reconnect"];
|
|
37
|
+
if(!v.cases||!v.comparisons)return ["Retained learning linked — /grants learning"];
|
|
38
|
+
const reasons=v.comparisons.map(c=>`${c.title??"Comparison"}: ${c.reason??c.state??"not ready"}`);
|
|
39
|
+
return [`${v.cases.length} case batch(es); ${v.comparisons.length} comparison(s)`,...reasons.slice(0,2),`Automatic questions: ${v.trust?.automaticExposure?.mode??"silent"} — ${v.trust?.automaticExposure?.reason??v.trust?.reason??"not configured"}`,"/grants learning opens deliberate review; not earned exposure"];
|
|
40
|
+
}
|
|
41
|
+
const finished = (a: DailyAttempt) => ["completed", "failed"].includes(a.intentRuntime) || a.archive?.runtime === "terminal";
|
|
42
|
+
|
|
43
|
+
/** Ordinary presentation is not another source of authority. All history remains under Details. */
|
|
44
|
+
export function renderDailyPanel(view: DailyView, width = 80, options: PanelOptions = {}): string {
|
|
45
|
+
const controls = options.controls as { ordinary?: { admission?: string; children?: { target: { executionId: string }; state: string; abortRequested: boolean }[] }; dispatch?: { paused?: boolean } } | undefined;
|
|
46
|
+
const live = controls?.ordinary?.children ?? [];
|
|
47
|
+
const rows = new Map(view.attempts.map(a => [a.executionId, a]));
|
|
48
|
+
const activeIds = options.connected && controls?.ordinary ? live.filter(r => r.state === "active").map(r => r.target.executionId) : view.attempts.filter(a => !finished(a)).map(a => a.executionId);
|
|
49
|
+
const complete = view.attempts.filter(finished);
|
|
50
|
+
const paused = controls?.dispatch?.paused || controls?.ordinary?.admission === "held-by-original-owner";
|
|
51
|
+
const failed=complete.filter(a=>a.intentRuntime==="failed"||a.archive?.outcome?.failed);
|
|
52
|
+
const unhealthy = options.error || options.control === "failed" || options.control === "unknown" || live.some(row=>row.state==="unknown");
|
|
53
|
+
const short=(value:string,rows=1)=>{const limit=Math.max(12,width-4)*rows;return [...panelText(value)].length>limit?[...panelText(value)].slice(0,limit-3).join("")+"...":value;};
|
|
54
|
+
const status = unhealthy || (!activeIds.length&&failed.length) ? "Needs you" : activeIds.length ? "Running" : paused ? "Waiting" : !view.obligations.length ? "Waiting" : view.obligations.every(o => o.attempts.some(id => rows.get(id) && finished(rows.get(id)!))) ? "Finished" : "Waiting";
|
|
55
|
+
const lines = ["PI-DADDY", options.presentation?.outcome ? short(options.presentation.outcome,2) : (view.scope ? "Selected work — outcome text unavailable" : "No work selected"), "", status];
|
|
56
|
+
if (unhealthy) lines.push("Control or observation needs attention. Open Details; no action was retried.");
|
|
57
|
+
else if (!activeIds.length&&failed.length) lines.push(`${failed.length} attempt(s) failed. Inspect results in /grants work.`);
|
|
58
|
+
else if (activeIds.length) lines.push(`${activeIds.length} active agent${activeIds.length === 1 ? "" : "s"}${paused ? "; new work paused" : ""}`);
|
|
59
|
+
else if (paused) lines.push("New work is paused. Running work is not cancelled by pausing.");
|
|
60
|
+
else if (status === "Finished") lines.push("Attempts finished. Review results; completion is not acceptance.");
|
|
61
|
+
else lines.push(view.obligations.length ? "Ready for an explicit run in /grants work. No automatic dispatch." : "Declare work with /grants work.");
|
|
62
|
+
for (const id of activeIds.slice(0, 8)) {
|
|
63
|
+
const a = rows.get(id), obligation = view.obligations.find(o => o.attempts.includes(id));
|
|
64
|
+
const label = options.presentation?.obligations.find(o => o.digest === obligation?.obligation.digest);
|
|
65
|
+
const original = live.find(row => row.target.executionId === id);
|
|
66
|
+
lines.push(` ${activeIds.indexOf(id)+1}. ${label?.agent || "Agent"} — ${original?.abortRequested ? "Stopping (awaiting settlement)" : options.connected ? "Running" : "Last observed active; freshness unknown"}`,
|
|
67
|
+
` ${short(label?.outcome || "Attempt details pending capture")}`,
|
|
68
|
+
` ${a?.modelId || "model unknown"} / effort ${a?.effortId || "unknown"}`);
|
|
69
|
+
}
|
|
70
|
+
if (activeIds.length > 8) lines.push(` ${activeIds.length - 8} more active agents in Details`);
|
|
71
|
+
if (complete.length) lines.push(`Finished attempts: ${complete.length} (collapsed; Details)`);
|
|
72
|
+
if (view.obligations.length > 1) lines.push(`Work: ${view.obligations.length} obligations; /grants work to select or change setup`);
|
|
73
|
+
if (!options.connected) lines.push("Snapshot only — live connection unavailable. /grants host to connect.");
|
|
74
|
+
if (view.continuity === "reconnected-gap") lines.push("Reconnected — earlier observation gap retained.");
|
|
75
|
+
if (options.learning?.length) lines.push("", "Learning", ...options.learning);
|
|
76
|
+
if (options.actions) lines.push("", "Available actions", ...(options.actions.length ? options.actions.map(a => `${a.choice} ${short(a.label,2)}`) : ["None currently authorized"]));
|
|
77
|
+
lines.push("", "d Details / history", "Pi coordinates work; skill-harness retains learning evidence.");
|
|
78
|
+
const summary = panelLines(lines, width);
|
|
79
|
+
return options.details ? `${summary}\n\n${panelLines([options.presentation?.outcome??"",...(options.presentation?.obligations.map(o=>o.outcome)??[])],width)}\n${renderDailyDetails(view, width)}${options.error ? `\n${panelLines([options.error], width)}` : ""}` : summary;
|
|
80
|
+
}
|
package/src/daily-view-render.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { DailyView } from "./daily-view.ts";
|
|
2
|
+
export { renderDailyPanel as renderDailyView } from "./daily-panel.ts";
|
|
2
3
|
const clean = (value: unknown) => String(value ?? "unknown").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
|
|
3
4
|
/** Text-only, accessible labels. No command links, control actions, model calls or terminal input. */
|
|
4
|
-
export function
|
|
5
|
+
export function renderDailyDetails(view: DailyView, width = 100): string {
|
|
5
6
|
const lines = ["PI-DADDY — READ-ONLY DAILY VIEW", view.narrative,
|
|
6
7
|
`Scope: ${view.scope ? `${view.scope.id} revision ${view.scope.revision} digest ${view.scope.digest}` : "UNRESOLVED — select an exact P01 snapshot"}`,
|
|
7
8
|
`Acceptance authority: ${view.authority}; coverage: PARTIAL; freshness: SNAPSHOT / UNKNOWN`,
|
package/src/daily-view.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface DailyAttempt {
|
|
|
18
18
|
executionId: string; logicalChildId: string | null; intentRuntime: string;
|
|
19
19
|
archive: ArchiveExecution | null; sourceAvailability: Array<{ sha256: string; state: "available" | "missing" | "mismatch" | "error" }>;
|
|
20
20
|
issues: string[]; activeBranch: null;
|
|
21
|
+
/** Observed launch configuration, not a claim about provider-internal reasoning. */
|
|
22
|
+
modelId?: string | null; effortId?: string | null;
|
|
21
23
|
}
|
|
22
24
|
export interface DailyObligation {
|
|
23
25
|
key: string; intent: RevisionRef; obligation: RevisionRef; policy: RevisionRef;
|
|
@@ -41,6 +43,10 @@ function contextCopy(value: DailyViewOptions["workContext"]): WorkProjectionCont
|
|
|
41
43
|
if (projectWorkLedger("", copied).errors.some(e => e.code === "WORK_CONTEXT_INVALID")) throw new TypeError("invalid independent P01 context");
|
|
42
44
|
return copied;
|
|
43
45
|
}
|
|
46
|
+
function observedLabel(labels: readonly { modelId: string | null; effortId: string | null }[], key: "modelId" | "effortId"): string | null {
|
|
47
|
+
const values = [...new Set(labels.map(label => label[key]))];
|
|
48
|
+
return values.length === 1 ? values[0] : null;
|
|
49
|
+
}
|
|
44
50
|
const readers = new WeakSet<object>();
|
|
45
51
|
/** Only actual factory readers may carry continuity state through the dashboard; not arbitrary callbacks. */
|
|
46
52
|
export function isDailyViewReader(value: unknown): value is ReturnType<typeof createDailyViewReader> {
|
|
@@ -107,6 +113,8 @@ export function createDailyViewReader() {
|
|
|
107
113
|
for (const attempt of work.runtime?.attempts ?? []) attempts.set(attempt.executionId, {
|
|
108
114
|
executionId: attempt.executionId, logicalChildId: attempt.childId, intentRuntime: attempt.state, archive: null,
|
|
109
115
|
sourceAvailability: [], issues: attempt.problems.map(p => `P01:${p.code}`), activeBranch: null,
|
|
116
|
+
...(observedLabel(attempt.observedLabels, "modelId")!==null?{modelId:observedLabel(attempt.observedLabels,"modelId")}:{}),
|
|
117
|
+
...(observedLabel(attempt.observedLabels, "effortId")!==null?{effortId:observedLabel(attempt.observedLabels,"effortId")}:{}),
|
|
110
118
|
});
|
|
111
119
|
for (const entry of archive) {
|
|
112
120
|
const a = attempts.get(entry.executionId) ?? { executionId: entry.executionId, logicalChildId: null, intentRuntime: "unknown",
|
package/src/dashboard-cli.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { parseDashboardLedger } from "./dashboard-projection.ts";
|
|
|
10
10
|
import { renderDashboard } from "./dashboard-render.ts";
|
|
11
11
|
import { createDailyViewReader, isDailyViewReader, readDailyView, type DailyViewOptions } from "./daily-view.ts";
|
|
12
12
|
import { renderDailyView } from "./daily-view-render.ts";
|
|
13
|
+
import { renderDailyPanel, panelLines, learningSummary, type PanelAction, type WorkPresentation } from "./daily-panel.ts";
|
|
14
|
+
import { createDashboardMenu } from "./dashboard-menu.ts";
|
|
13
15
|
import { projectWorkLedger, type WorkProjectionContext } from "./work-ledger.ts";
|
|
14
16
|
import { parseRetentionJson } from "./retention-json.ts";
|
|
15
17
|
import { createInterface } from "node:readline";
|
|
@@ -49,6 +51,7 @@ export interface DashboardFrameOptions {
|
|
|
49
51
|
debrief?: DebriefPresenter;
|
|
50
52
|
debriefJson?: boolean;
|
|
51
53
|
connected?: DashboardConnection;
|
|
54
|
+
actionMenu?: ReturnType<typeof createDashboardMenu>;
|
|
52
55
|
}
|
|
53
56
|
export async function dashboardHostAction(host:DashboardConnection,line:string){
|
|
54
57
|
if(!isDashboardConnection(host)||Buffer.byteLength(line)>60000)throw Error("genuine bounded dashboard connection required");
|
|
@@ -60,10 +63,10 @@ export async function dashboardHostAction(host:DashboardConnection,line:string){
|
|
|
60
63
|
|
|
61
64
|
/** A fulfilled host call is not necessarily an applied native effect; keep those facts distinct in the UI. */
|
|
62
65
|
export function dashboardActionFeedback(command:string,outcome:unknown):string{
|
|
63
|
-
const value=outcome&&typeof outcome==="object"?outcome as {state?:unknown;result?:unknown}:{};
|
|
66
|
+
const value=outcome&&typeof outcome==="object"?outcome as {state?:unknown;result?:unknown;error?:unknown;reason?:unknown}:{};
|
|
64
67
|
const state=typeof value.state==="string"?value.state:"unrecognised host response";
|
|
65
68
|
if(state==="failed-or-unknown"||state==="readback-only")return `NO ACTION CLAIM: ${command} returned ${state}; inspect/reconcile explicitly.`;
|
|
66
|
-
if(state!=="acknowledged")return `NOT APPLIED: ${command} returned ${state}
|
|
69
|
+
if(state!=="acknowledged")return `NOT APPLIED: ${command} returned ${state}.${typeof (value.error??value.reason)==="string"?` ${String(value.error??value.reason).slice(0,1024)}`:""}`;
|
|
67
70
|
const result=value.result&&typeof value.result==="object"?value.result as {application?:unknown}:null;
|
|
68
71
|
// A retained records[] snapshot has no outer dashboard request correlation. Never select an old receipt.
|
|
69
72
|
const nativeApplication=typeof result?.application==="string"?result.application:undefined;
|
|
@@ -110,15 +113,14 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
110
113
|
if(options.connected){
|
|
111
114
|
if(!isDashboardConnection(options.connected))throw Error("original dashboard connection required");
|
|
112
115
|
let view:Awaited<ReturnType<DashboardConnection["frame"]>>;
|
|
113
|
-
try{view=await options.connected.frame();}catch(error){return
|
|
116
|
+
try{view=await options.connected.frame();}catch(error){options.actionMenu?.clear();return panelLines(["PI-DADDY — Disconnected", "The original host is unavailable. No action was retried.", "Return to Pi: /grants host, then /grants dashboard.", ...(options.details?[String(error)]:[])],options.width);}
|
|
114
117
|
if(options.dailyJson||options.debriefJson)return JSON.stringify(view);
|
|
115
118
|
const source=view.source as {daily?:Parameters<typeof renderDailyView>[0]}|null;
|
|
116
|
-
const actions=(view.actions as
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
actions.length?`ACTIONS — type one command and Enter\n${actions.map(action=>` ${action.key} — ${action.label}`).join("\n")}`:"ACTIONS — none currently authorized; refresh never acts."].filter((line):line is string=>Boolean(line)).join("\n\n");
|
|
119
|
+
const actions=(view.actions as PanelAction[]|undefined)??[];
|
|
120
|
+
const menu=(options.actionMenu??createDashboardMenu()).show(view.tip,actions);
|
|
121
|
+
const learning=view.debrief?["Retained review open below"]:learningSummary(view.learning);
|
|
122
|
+
const summary=source?.daily?renderDailyPanel(source.daily,options.width,{details:options.details,connected:true,presentation:view.presentation as WorkPresentation|null,error:view.error,controls:view.controls,control:view.control,actions:menu,learning}):panelLines(["PI-DADDY — Needs you","Work observation unavailable. Capture current work or open Details.",...menu.map(a=>`${a.choice} ${a.label}`)],options.width);
|
|
123
|
+
return [summary,view.debrief?renderDebrief(view.debrief as Parameters<typeof renderDebrief>[0],options.width):"",options.details?panelLines([`Host control ${view.control}; acknowledgement ${view.acknowledgement}. Attention reserved ${view.attention.attentionUsed}/5. Tip ${view.tip}.`,JSON.stringify(view.controls),JSON.stringify(view.learning),...actions.map(a=>`${a.key} — ${a.label}`)],options.width):""].filter(Boolean).join("\n\n");
|
|
122
124
|
}
|
|
123
125
|
if (options.debrief || options.debriefJson) {
|
|
124
126
|
if (!isDebriefPresenter(options.debrief)) return "PI-DADDY — DEBRIEF UNAVAILABLE: genuine presenter/host missing";
|
|
@@ -128,7 +130,7 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
|
|
|
128
130
|
try {
|
|
129
131
|
if (options.dailyReader !== undefined && !isDailyViewReader(options.dailyReader)) throw new TypeError("untrusted daily reader callback");
|
|
130
132
|
const view = await (options.dailyReader ?? readDailyView)(options.dailyView);
|
|
131
|
-
return options.dailyJson ? JSON.stringify(view) :
|
|
133
|
+
return options.dailyJson ? JSON.stringify(view) : renderDailyPanel(view, options.width, { details: options.details });
|
|
132
134
|
} catch {
|
|
133
135
|
return "PI-DADDY — DAILY VIEW UNAVAILABLE\nInvalid or unavailable read-only input. No previous acceptance or continuity was substituted.";
|
|
134
136
|
}
|
|
@@ -227,6 +229,8 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
227
229
|
if (!host.debrief) await debrief.open({ mode: "manual", userPresent: true }); // Explicit CLI fixture only; never override a host deferral.
|
|
228
230
|
}
|
|
229
231
|
const dailyReader = createDailyViewReader();
|
|
232
|
+
const actionMenu=createDashboardMenu();
|
|
233
|
+
let details=cli.details;
|
|
230
234
|
let previous = "";
|
|
231
235
|
let feedback = "";
|
|
232
236
|
let input: ReturnType<typeof createInterface> | null = null;
|
|
@@ -237,13 +241,13 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
237
241
|
protocol,
|
|
238
242
|
color: cli.color,
|
|
239
243
|
width: process.stdout.columns || 80,
|
|
240
|
-
details
|
|
244
|
+
details, actionMenu,
|
|
241
245
|
dailyView, dailyReader, dailyJson: cli.dailyJson, debrief, debriefJson: cli.debriefJson, connected,
|
|
242
246
|
});
|
|
243
|
-
const rendered = feedback ? `${
|
|
247
|
+
const rendered = feedback ? `${panelLines([feedback], process.stdout.columns || 80)}\n\n${frame}` : frame;
|
|
244
248
|
if (rendered === previous && !clear) return;
|
|
245
249
|
previous = rendered;
|
|
246
|
-
const prompt = "
|
|
250
|
+
const prompt = "Action number / d Details, then Enter: ";
|
|
247
251
|
const draft = input?.line ?? "";
|
|
248
252
|
process.stdout.write(clear ? `\u001b]0;PI-DADDY\u0007\u001b[2J\u001b[H${rendered}\n\n${input ? prompt + draft : ""}` : `${rendered}\n`);
|
|
249
253
|
if (input) input.setPrompt(prompt);
|
|
@@ -253,7 +257,7 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
253
257
|
await draw(false);
|
|
254
258
|
return;
|
|
255
259
|
}
|
|
256
|
-
input =
|
|
260
|
+
input = createInterface({ input: process.stdin, output: process.stdout, terminal: Boolean(process.stdin.isTTY) });
|
|
257
261
|
await draw(true);
|
|
258
262
|
let drawing = false;
|
|
259
263
|
const redraw = (): void => {
|
|
@@ -264,10 +268,18 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
|
|
|
264
268
|
let acting = false;
|
|
265
269
|
input?.on("line", line => {
|
|
266
270
|
if (acting) return; // never replay a line typed while the prior exact command is still settling.
|
|
271
|
+
if(line.trim().toLowerCase()==="d"){details=!details;redraw();return;}
|
|
267
272
|
acting = true;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
let label=line.trim()||"(empty)";
|
|
274
|
+
const act=async()=>{
|
|
275
|
+
if(connected&&/^\d+$/.test(line.trim())){const selected=actionMenu.select(line.trim());label=selected.label;return connected.humanAction(selected.key,{tip:selected.tip,requestDigest:selected.requestDigest});}
|
|
276
|
+
if(connected)return dashboardHostAction(connected,line);
|
|
277
|
+
if(debrief)return debriefAction(debrief,line);
|
|
278
|
+
throw Error("Read-only snapshot; no controls connected");
|
|
279
|
+
};
|
|
280
|
+
feedback = `Action ${label} — awaiting acknowledgement…`;
|
|
281
|
+
void act().then(outcome => {
|
|
282
|
+
feedback = connected ? dashboardActionFeedback(label, outcome) : `ACKNOWLEDGED: ${label}. Refreshed state is shown below.`;
|
|
271
283
|
}).catch(error => {
|
|
272
284
|
feedback = `REJECTED: ${error instanceof Error ? error.message : String(error)}. No command was retried; inspect the refreshed offered keys.`;
|
|
273
285
|
}).finally(() => { acting = false; redraw(); });
|
|
@@ -32,6 +32,8 @@ export interface DashboardHostConfig {
|
|
|
32
32
|
budgetDigest: string; experimentDigest: string | null; harnessArtifactDigest: string;
|
|
33
33
|
ordinaryDigest?: string;
|
|
34
34
|
learningLifecycleId?: string;
|
|
35
|
+
/** Explicit host journal, while reusing the original trust store/attention across host lifetimes. */
|
|
36
|
+
journalDirectory?: string;
|
|
35
37
|
}
|
|
36
38
|
export interface DashboardHostRequest { version: "1.0"; requestId: string; hostDigest: string; expectedTip: string; selectionDigest: string; operation: "observe" | "present" | "presented" | "defer" | "debrief" | "dispatch" | "intent" | "cancel" | "dispatch-reconcile" | "intent-reconcile" | "ordinary-cancel"; payload: unknown }
|
|
37
39
|
export interface DashboardHostAuthority {
|
|
@@ -13,10 +13,13 @@ export async function serveDashboardHost(path:string,host:DashboardHost){
|
|
|
13
13
|
if(!isDashboardHost(host))throw Error("original dashboard host required");await parent(path);const clients=new Set<Socket>(),tasks=new Set<Promise<void>>();
|
|
14
14
|
const server=createServer(socket=>{if(clients.size>=4){socket.destroy();return;}clients.add(socket);socket.on("close",()=>clients.delete(socket));socket.on("error",()=>{});socket.setTimeout(5000,()=>socket.destroy());let size=0,used=false;const chunks:Buffer[]=[];
|
|
15
15
|
socket.on("data",bytes=>{if(used)return;size+=bytes.length;if(size>65536){socket.destroy();return;}chunks.push(bytes);if(bytes.at(-1)!==10)return;used=true;
|
|
16
|
-
const task=(async()=>{try{const text=new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks));const r=parseRetentionJson(text,65536) as {operation:string;request?:DashboardHostRequest;key?:string};let result:unknown;
|
|
16
|
+
const task=(async()=>{try{const text=new TextDecoder("utf-8",{fatal:true}).decode(Buffer.concat(chunks));const r=parseRetentionJson(text,65536) as {operation:string;request?:DashboardHostRequest;key?:string;expected?:{tip:string;requestDigest:string}};let result:unknown;
|
|
17
17
|
if(Object.keys(r).join()==="operation"&&r.operation==="frame")result=await host.frame();
|
|
18
18
|
else if(Object.keys(r).sort().join()==="operation,request"&&r.operation==="action")result=await host.action(dashboardHostRequest(r.request!));
|
|
19
|
-
else if(Object.keys(r).sort().join()
|
|
19
|
+
else if(["key,operation","expected,key,operation"].includes(Object.keys(r).sort().join())&&r.operation==="human-action"&&typeof r.key==="string"){
|
|
20
|
+
if(r.expected&&(!/^[a-f0-9]{64}$/.test(r.expected.tip)||!/^[a-f0-9]{64}$/.test(r.expected.requestDigest)||Object.keys(r.expected).sort().join()!=="requestDigest,tip"))throw Error("exact displayed action required");
|
|
21
|
+
result=await host.humanAction(r.key,r.expected);
|
|
22
|
+
}
|
|
20
23
|
else throw Error("closed dashboard protocol required");
|
|
21
24
|
const output=JSON.stringify({ok:true,result})+"\n";if(Buffer.byteLength(output)>512*1024)throw Error("dashboard frame bound exceeded");socket.end(output);
|
|
22
25
|
}catch{socket.end(JSON.stringify({ok:false,error:"refused or acknowledgement unknown; explicit readback only"})+"\n");}})();tasks.add(task);void task.finally(()=>tasks.delete(task));
|
|
@@ -28,7 +31,7 @@ export async function serveDashboardHost(path:string,host:DashboardHost){
|
|
|
28
31
|
}
|
|
29
32
|
/** Explicit operator-selected endpoint. Socket ownership is access policy, not module/human authentication. */
|
|
30
33
|
export function connectDashboardHost(path:string):DashboardConnection{
|
|
31
|
-
const call=async(operation:string,payload:{request?:DashboardHostRequest;key?:string}={})=>{await parent(path);const s=await lstat(path);if(!s.isSocket()||s.mode&0o077||s.uid!==process.getuid?.())throw Error("private original dashboard endpoint unavailable");
|
|
34
|
+
const call=async(operation:string,payload:{request?:DashboardHostRequest;key?:string;expected?:{tip:string;requestDigest:string}}={})=>{await parent(path);const s=await lstat(path);if(!s.isSocket()||s.mode&0o077||s.uid!==process.getuid?.())throw Error("private original dashboard endpoint unavailable");
|
|
32
35
|
return new Promise<any>((resolve,reject)=>{const socket=createConnection(path);let size=0,done=false;const chunks:Buffer[]=[];const fail=(e:unknown)=>{if(!done){done=true;socket.destroy();reject(e);}};
|
|
33
36
|
socket.setTimeout(5000,()=>fail(Error("dashboard acknowledgement timeout; do not repeat action")));socket.on("error",fail);socket.on("connect",()=>socket.write(JSON.stringify({operation,...payload})+"\n"));
|
|
34
37
|
socket.on("data",b=>{size+=b.length;if(size>512*1024){fail(Error("dashboard frame limit"));return;}chunks.push(b);});
|
|
@@ -36,5 +39,5 @@ export function connectDashboardHost(path:string):DashboardConnection{
|
|
|
36
39
|
socket.on("close",()=>{if(!done)fail(Error("dashboard disconnected; acknowledgement unknown"));});
|
|
37
40
|
});
|
|
38
41
|
};
|
|
39
|
-
const api={frame:()=>call("frame"),action:(r:DashboardHostRequest)=>call("action",{request:dashboardHostRequest(r)}),humanAction:(key:string)=>call("human-action",{key})};connections.add(api);return Object.freeze(api);
|
|
42
|
+
const api={frame:()=>call("frame"),action:(r:DashboardHostRequest)=>call("action",{request:dashboardHostRequest(r)}),humanAction:(key:string,expected?:{tip:string;requestDigest:string})=>call("human-action",{key,...(expected?{expected}:{})})};connections.add(api);return Object.freeze(api);
|
|
40
43
|
}
|
package/src/dashboard-host.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkPresentation } from "./daily-panel.ts";
|
|
1
2
|
import { isOrdinaryChildren, holdOrdinaryDispatch, type OrdinaryChildren, type OrdinaryCancellation } from "./ordinary-children.ts";
|
|
2
3
|
import { loadedDashboardHarnessDigest } from "./dashboard-harness.ts";
|
|
3
4
|
import { join, isAbsolute } from "node:path";
|
|
@@ -22,6 +23,11 @@ export type DashboardHost=ReturnType<typeof openDashboardHost>;
|
|
|
22
23
|
export interface DashboardHostOptions { ordinary?:OrdinaryChildren; harness:DashboardHarness; config:DashboardHostConfig; budget:GovernedBudgetBinding; experiment?:ReturnType<typeof openExperiment>; authority:()=>DashboardHostAuthority|null;
|
|
23
24
|
/** Exact actions constructed and authorized by the owner; the dashboard exposes only human labels/keys. */
|
|
24
25
|
humanActions?:(context:{hostDigest:string;selectionDigest:string;tip:string;observations:readonly {sourceId:string;checkpointId:string}[];preparedPresentationDigest:string|null})=>readonly {key:string;label:string;request:DashboardHostRequest}[]|Promise<readonly {key:string;label:string;request:DashboardHostRequest}[]>;
|
|
26
|
+
/** Read-only, digest-bound local outcome labels. Never grants acceptance or control. */
|
|
27
|
+
presentation?: () => WorkPresentation | null | Promise<WorkPresentation | null>;
|
|
28
|
+
currentWork?: () => Promise<import("./daily-view.ts").DailyView>;
|
|
29
|
+
learningView?: () => unknown;
|
|
30
|
+
afterObservation?: (observation: import("./dashboard-observation.ts").DashboardObservation) => void;
|
|
25
31
|
/** Explicit approved observation effect only; refresh/frame never invokes it. */
|
|
26
32
|
beforeObservation?:(sourceId:string)=>Promise<void>;
|
|
27
33
|
/** Owner callback after the exact intent journal reports applied; failure keeps host acknowledgement unknown. */
|
|
@@ -30,7 +36,8 @@ export interface DashboardHostOptions { ordinary?:OrdinaryChildren; harness:Dash
|
|
|
30
36
|
presence?:()=>{present:boolean;closing:boolean;evidenceDigest:string;expiresAt:number}|null;
|
|
31
37
|
}
|
|
32
38
|
function configuration(input:DashboardHostConfig){
|
|
33
|
-
const c=detached(input);controlShape(c,["version","trustDirectory","trustPolicyId","archiveRoot","scope","author","policyPath","policySha256","sources","selection","cases","blind","budgetDigest","experimentDigest","harnessArtifactDigest",...(Object.hasOwn(c,"ordinaryDigest")?["ordinaryDigest"]:[]),...(Object.hasOwn(c,"learningLifecycleId")?["learningLifecycleId"]:[])]);
|
|
39
|
+
const c=detached(input);controlShape(c,["version","trustDirectory","trustPolicyId","archiveRoot","scope","author","policyPath","policySha256","sources","selection","cases","blind","budgetDigest","experimentDigest","harnessArtifactDigest",...(Object.hasOwn(c,"ordinaryDigest")?["ordinaryDigest"]:[]),...(Object.hasOwn(c,"learningLifecycleId")?["learningLifecycleId"]:[]),...(Object.hasOwn(c,"journalDirectory")?["journalDirectory"]:[])]);
|
|
40
|
+
if(c.journalDirectory!==undefined&&(typeof c.journalDirectory!=="string"||!isAbsolute(c.journalDirectory)))throw Error("explicit host journal directory required");
|
|
34
41
|
if(Object.hasOwn(c,"ordinaryDigest")&&!sha(c.ordinaryDigest))throw Error("invalid ordinary binding digest");
|
|
35
42
|
if(Object.hasOwn(c,"learningLifecycleId")&&!sha(c.learningLifecycleId))throw Error("invalid learning lifecycle manifest");
|
|
36
43
|
if(c.version!=="producer-dashboard-host-v1"||![c.trustDirectory,c.archiveRoot,c.policyPath].every(p=>typeof p==="string"&&isAbsolute(p))||![c.trustPolicyId,c.policySha256,c.budgetDigest,c.harnessArtifactDigest].every(sha)||!(c.experimentDigest===null||sha(c.experimentDigest))||typeof c.scope!=="string"||!c.scope||c.scope.length>128||typeof c.author!=="string"||!c.author||c.author.length>256||!Array.isArray(c.sources)||c.sources.length<1||c.sources.length>32||new Set(c.sources.map(s=>s.id)).size!==c.sources.length||c.sources.filter(s=>s.kind==="work").length>1||c.sources.some(s=>!/^[a-zA-Z0-9:_-]{1,128}$/.test(s.id)||!["work","retention","facts"].includes(s.kind)))throw Error("invalid explicit dashboard host binding");
|
|
@@ -43,7 +50,7 @@ export function createDashboardHost(options:DashboardHostOptions){
|
|
|
43
50
|
if(!a?.hostDigests.includes(dashboardHostDigest(c)))throw Error("independent exact host authority required");
|
|
44
51
|
const trust=h.openTrustLifecycle(c.trustDirectory),view=trust.inspect(Date.now());
|
|
45
52
|
if(view.policyId!==c.trustPolicyId)throw Error("trust policy changed");
|
|
46
|
-
h.learningJournal(join(c.trustDirectory,"producer-host"),{type:"producer-host-initial",config:c});
|
|
53
|
+
h.learningJournal(c.journalDirectory??join(c.trustDirectory,"producer-host"),{type:"producer-host-initial",config:c});
|
|
47
54
|
return openDashboardHost(options);
|
|
48
55
|
}
|
|
49
56
|
/** Existing native writers + original controllers; no model, launch, authority-file loader or PID recovery. */
|
|
@@ -53,7 +60,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
53
60
|
const budget=openResourceBudget(options.budget),experiment=options.experiment,ordinary=options.ordinary;
|
|
54
61
|
if(ordinary&&(!isOrdinaryChildren(ordinary)||ordinary.bindingDigest!==c.ordinaryDigest))throw Error("original ordinary controller binding required");
|
|
55
62
|
if(resourceBindingDigest(budget.binding)!==c.budgetDigest||experiment&&(!isExperimentController(experiment)||experimentBindingDigest(experiment.binding)!==c.experimentDigest))throw Error("original controller binding required");
|
|
56
|
-
const trust=h.openTrustLifecycle(c.trustDirectory),journal=h.learningJournal(join(c.trustDirectory,"producer-host"));
|
|
63
|
+
const trust=h.openTrustLifecycle(c.trustDirectory),journal=h.learningJournal(c.journalDirectory??join(c.trustDirectory,"producer-host"));
|
|
57
64
|
const initial=journal.read()[0].value;
|
|
58
65
|
if(initial.type!=="producer-host-initial"||dataDigest(initial.config)!==hostDigest)throw Error("host store binding changed; no new budget");
|
|
59
66
|
const trustInput=trust.history()[0].value.input as {archiveRoot:string;component:unknown;exposure:{attentionRemaining:number}|null};
|
|
@@ -62,7 +69,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
62
69
|
const observation=dashboardObservations(h,c,journal.read),history=()=>journal.read();
|
|
63
70
|
const append=(value:Record<string,unknown>)=>journal.append(history().at(-1)!.id,value);
|
|
64
71
|
const saved=():DebriefCheckpoint|null=>{const rows=history().filter(e=>e.value.type==="checkpoint");return rows.length?detached(rows.at(-1)!.value.checkpoint) as DebriefCheckpoint:null;};
|
|
65
|
-
let presenter:DebriefPresenter|undefined,busy=false,poisoned=false,presentationRevision:number|null=null,presenceDigest:string|null=null,attentionDeferred:string|null=null,presentationAcknowledged=false;
|
|
72
|
+
let presenter:DebriefPresenter|undefined,busy=false,poisoned=false,stopped=false,presentationRevision:number|null=null,presenceDigest:string|null=null,attentionDeferred:string|null=null,presentationAcknowledged=false;
|
|
66
73
|
const ordinaryDispatchHoldKey=dataDigest({hostDigest,control:"pause-new-ordinary-dispatch"});
|
|
67
74
|
// A host frame is a read-only snapshot: ordinary children may attach or settle without advancing this journal.
|
|
68
75
|
// Within one tip, a key keeps its first exact request; humanAction revalidates it against current native authority.
|
|
@@ -88,39 +95,60 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
88
95
|
return {key:x.key,label:x.label,operation:request.operation,request};});
|
|
89
96
|
};
|
|
90
97
|
const api={hostDigest,selectionDigest,
|
|
98
|
+
/** Same owner gate as action: checking stop cannot race a newly admitted effect. Never releases a hold. */
|
|
99
|
+
async stop(){
|
|
100
|
+
if(stopped)return;if(busy)throw Error("Cannot stop: dashboard operation busy; wait for its acknowledgement first");busy=true;
|
|
101
|
+
try{
|
|
102
|
+
const rows=history();
|
|
103
|
+
if(poisoned||rows.some(e=>e.value.type==="host-failure")||rows.some(e=>e.value.type==="claim"&&!rows.some(r=>r.value.type==="result"&&r.value.requestId===e.value.requestId)))throw Error("Cannot stop: acknowledgement unknown; retain this original host for readback. Do not repeat effects. Ending the whole Pi session does not reconcile or release this hold.");
|
|
104
|
+
if(c.ordinaryDigest&&!ordinary)throw Error("Cannot stop: original ordinary boundary unavailable; no recovery");
|
|
105
|
+
const dispatch=await budget.controls(null).inspect();
|
|
106
|
+
if(dispatch.admission==="blocked-pending")throw Error("Cannot stop: pending original control requires explicit reconciliation, then resume before stopping");
|
|
107
|
+
if(dispatch.paused||ordinary&&(ordinary.inspect() as {admission:string}).admission!=="open")throw Error("Cannot stop: resume new work in the original dashboard first. Any pending intent also needs its original reconciliation; no hold was released.");
|
|
108
|
+
stopped=true;presenter?.close();presentationRevision=null;
|
|
109
|
+
}finally{busy=false;}
|
|
110
|
+
},
|
|
111
|
+
/** Final owner-session disposal only, not /grants host stop. Disconnects without resolving/releasing any control. */
|
|
112
|
+
endSession(){stopped=true;presenter?.close();presentationRevision=null;},
|
|
91
113
|
async frame(){
|
|
92
114
|
const rows=history(),tip=rows.at(-1)!.id,a=authority();let source:unknown=null,error:string|null=null;
|
|
93
115
|
let selection=c.selection,selectionState="current-control-snapshot";
|
|
94
116
|
try{selection=await currentSelection();}catch(e){error=String(e);selectionState="unavailable-host-declaration-only";}
|
|
95
|
-
try{source=await observation.view(selectionState==="current-control-snapshot"?a:null,selection);
|
|
117
|
+
try{source=await observation.view(selectionState==="current-control-snapshot"?a:null,selection);
|
|
118
|
+
if(options.currentWork)source={...(source as object),daily:await options.currentWork()};
|
|
119
|
+
}catch(e){error=String(e);}
|
|
96
120
|
let debrief:unknown=null;
|
|
97
121
|
if(a&&presenter&&presentationRevision!==null&&presence()?.evidenceDigest===presenceDigest){try{await paused(presentationRevision);debrief=presenter.view();}catch{debrief=null;}}
|
|
98
122
|
let controls:unknown=null;try{controls=await nativeRead();}catch(e){error=String(e);}
|
|
99
123
|
let learning:unknown=null;if(c.learningLifecycleId)try{learning=h.readLearningLifecycle(c.archiveRoot,c.learningLifecycleId);}catch(e){error=String(e);}
|
|
100
|
-
|
|
124
|
+
let presentation:WorkPresentation|null=null;try{presentation=await options.presentation?.()??null;}catch(e){error=String(e);}
|
|
125
|
+
if(options.learningView)try{learning=options.learningView();}catch(e){learning={state:"deferred",reason:String(e)};}
|
|
126
|
+
const attention=trust.inspect(Date.now());let actions:{key:string;label:string;operation:string;requestDigest:string}[]=[];
|
|
101
127
|
try{const available=await availableHumanActions();
|
|
102
128
|
// A frame that completed after its captured tip is stale and cannot replace a newer display generation.
|
|
103
|
-
if(history().at(-1)!.id===tip){
|
|
129
|
+
if(!stopped&&history().at(-1)!.id===tip){
|
|
104
130
|
if(!displayedActions||displayedActions.tip!==tip)displayedActions={tip,actions:new Map()};
|
|
105
131
|
const additions=available.filter(action=>!displayedActions!.actions.has(action.key));
|
|
106
132
|
if(displayedActions.actions.size+additions.length>MAX_DISPLAYED_ACTIONS_PER_TIP)throw Error("displayed dashboard action capacity exhausted");
|
|
107
133
|
const shown=available.filter(action=>{const prior=displayedActions!.actions.get(action.key);
|
|
108
134
|
if(prior&&dashboardHostRequestDigest(prior.request)!==dashboardHostRequestDigest(action.request))return false;
|
|
109
135
|
if(!prior)displayedActions!.actions.set(action.key,action);return true;});
|
|
110
|
-
actions=shown.map(({key,label,operation})=>({key,label,operation}));
|
|
136
|
+
actions=shown.map(({key,label,operation,request})=>({key,label,operation,requestDigest:dashboardHostRequestDigest(request)}));
|
|
111
137
|
}}catch(e){error=String(e);}
|
|
112
|
-
return freeze(detached({version:"producer-dashboard-frame-v1",hostDigest,selectionDigest:dashboardSelectionDigest(selection),selectionState,tip,source,controls,learning,debrief,attention,error,actions,
|
|
138
|
+
return freeze(detached({version:"producer-dashboard-frame-v1",hostDigest,selectionDigest:dashboardSelectionDigest(selection),selectionState,tip,source,controls,learning,debrief,attention,error,actions,presentation,
|
|
113
139
|
requests:rows.filter(e=>["claim","result","presentation","defer","ordinary-intent-pending"].includes(String(e.value.type))).map(e=>e.value),
|
|
114
140
|
control:rows.some(e=>e.value.type==="host-failure")?"failed":rows.some(e=>e.value.type==="claim"&&!rows.some(r=>r.value.type==="result"&&r.value.requestId===e.value.requestId))?"unknown":"not-assessed",
|
|
115
141
|
acknowledgement:poisoned?"unknown":"readback-only",identity:"independently-declared-host; not human/module authentication",activeBranch:null,acceptance:"not-assessed",freshness:"snapshot-unknown",workerInteractions:0}));
|
|
116
142
|
},
|
|
117
143
|
/** Explicit reconciliation is read-only. A retained claim is never replayed as an effect. */
|
|
118
144
|
reconcile:()=>api.frame(),
|
|
119
|
-
async humanAction(key:string){const tip=history().at(-1)!.id;if(!displayedActions||displayedActions.tip!==tip)throw Error("dashboard actions must be displayed at the current tip");
|
|
145
|
+
async humanAction(key:string,expected?:{tip:string;requestDigest:string}){const tip=history().at(-1)!.id;if(expected&&expected.tip!==tip)throw Error("displayed action is stale; select again");if(!displayedActions||displayedActions.tip!==tip)throw Error("dashboard actions must be displayed at the current tip");
|
|
120
146
|
const found=displayedActions.actions.get(key);if(!found)throw Error(`unknown dashboard action ${JSON.stringify(key)}`);
|
|
147
|
+
if(expected&&dashboardHostRequestDigest(found.request)!==expected.requestDigest)throw Error("displayed action identity changed; no effect attempted");
|
|
121
148
|
const current=(await availableHumanActions()).find(action=>action.key===key);if(!current||current.label!==found.label||dashboardHostRequestDigest(current.request)!==dashboardHostRequestDigest(found.request))throw Error("displayed dashboard action changed; no effect attempted");return api.action(found.request);},
|
|
122
149
|
async action(input:DashboardHostRequest){
|
|
123
150
|
const request=dashboardHostRequest(input),digest=dashboardHostRequestDigest(request);
|
|
151
|
+
if(stopped)throw Error("dashboard host stopped; no effect attempted");
|
|
124
152
|
if(busy||poisoned)throw Error("dashboard operation busy or acknowledgement unknown");busy=true;let attempted=false;
|
|
125
153
|
try{
|
|
126
154
|
const rows=history(),old=rows.find(e=>e.value.type==="claim"&&e.value.requestId===request.requestId);
|
|
@@ -135,7 +163,7 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
135
163
|
try{
|
|
136
164
|
if(request.operation==="observe"){
|
|
137
165
|
const sourceId=(request.payload as {sourceId?:unknown})?.sourceId;if(typeof sourceId!=="string")throw Error("exact observation source required");await options.beforeObservation?.(sourceId);
|
|
138
|
-
const captured=observation.observe(request.payload,a!,selection);append({type:"observation",observation:captured});result=captured;
|
|
166
|
+
const captured=observation.observe(request.payload,a!,selection);append({type:"observation",observation:captured});options.afterObservation?.(captured);result=captured;
|
|
139
167
|
}else if(request.operation==="present"){
|
|
140
168
|
const p=detached(request.payload) as {userPresent:boolean;closing:boolean;evidenceDigest:string;dispatchRevision:number};
|
|
141
169
|
if(Object.keys(p).sort().join()!=="closing,dispatchRevision,evidenceDigest,userPresent"||!sha(p.evidenceDigest)||typeof p.userPresent!=="boolean"||typeof p.closing!=="boolean"||!Number.isSafeInteger(p.dispatchRevision))throw Error("explicit presence/boundary evidence required");
|
|
@@ -165,8 +193,10 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
165
193
|
const port=budget.controls(a!.dispatch),ordinaryHold=ordinary&&["pause-dispatch","resume-dispatch"].includes(native.action)?holdOrdinaryDispatch(ordinary,ordinaryDispatchHoldKey):undefined;
|
|
166
194
|
if(request.operation==="dispatch-reconcile"&&(await port.inspect()).records.find(r=>r.request.requestId===native.requestId)?.digest!==dispatchRequestDigest(native))throw Error("exact original dispatch reconciliation required");
|
|
167
195
|
result=request.operation==="dispatch"?await port.request(native):await port.reconcile(native.requestId);
|
|
168
|
-
const
|
|
169
|
-
if(
|
|
196
|
+
const snapshot=result as import("./dispatch-control.ts").DispatchSnapshot,record=snapshot.records.find(r=>r.request.requestId===native.requestId);
|
|
197
|
+
if(!record)throw Error("native dispatch application acknowledgement unavailable");
|
|
198
|
+
// A denied resume must not undo an earlier pause/pending control. Release only after final host sync.
|
|
199
|
+
if(ordinaryHold&&(native.action==="resume-dispatch"&&record.decision==="approved"&&record.application==="applied"||record.application==="not-applied"&&snapshot.admission==="enabled"))releaseOrdinary=ordinaryHold.release;
|
|
170
200
|
}else if(request.operation==="intent"||request.operation==="intent-reconcile"){
|
|
171
201
|
const native=detached(intentRequest(request.payload as IntentRequest)) as IntentRequest,port=budget.intentControls(a!.dispatch),digest=intentRequestDigest(native);
|
|
172
202
|
if(c.ordinaryDigest&&!ordinary)throw Error("original ordinary boundary unavailable; no recovery");
|
|
@@ -202,6 +232,11 @@ export function openDashboardHost(options:DashboardHostOptions){
|
|
|
202
232
|
}else throw Error("unsupported dashboard operation");
|
|
203
233
|
}
|
|
204
234
|
}catch(error){presenter?.close();presentationRevision=null;append({type:"result",requestId:request.requestId,state:"failed-or-unknown",reason:String(error)});throw error;}
|
|
235
|
+
if(["dispatch","dispatch-reconcile","intent","intent-reconcile"].includes(request.operation)&&result&&typeof result==="object"){
|
|
236
|
+
const snapshot=result as {records?:{requestId?:string;request?:{requestId:string};application?:string}[]},nativeId=(request.payload as {requestId:string}).requestId;
|
|
237
|
+
const applied=snapshot.records?.find(r=>(r.requestId??r.request?.requestId)===nativeId);
|
|
238
|
+
if(applied?.application)result={...result,application:applied.application,nativeRequestId:nativeId};
|
|
239
|
+
}
|
|
205
240
|
append({type:"result",requestId:request.requestId,state:"acknowledged",resultDigest:dataDigest(result??null)});releaseOrdinary?.();return {state:"acknowledged",result};
|
|
206
241
|
}catch(error){if(attempted){poisoned=true;try{if(history().some(e=>e.value.type==="claim"&&e.value.requestId===request.requestId))append({type:"host-failure",requestId:request.requestId,reason:String(error)});}catch{/* Failure recording also unacknowledged; no claim of no effects. */}}throw error;}finally{busy=false;}
|
|
207
242
|
}
|