@milpa/comal 0.1.6 → 0.1.7
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/README.md +1 -1
- package/backend/contracts/remote-workspace-v1.schema.json +287 -0
- package/package.json +2 -1
- package/src/cli.mjs +3 -2
- package/src/remote-control.mjs +29 -4
- package/src/remote-workspace.mjs +50 -0
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ comal --help
|
|
|
28
28
|
|
|
29
29
|
Keep the PATH line in your shell startup file if you use that prefix. If your Node manager already provides a user prefix, you can omit `--prefix "$HOME/.local"`. The package registers the `comal` executable; you do not need to create a shell alias.
|
|
30
30
|
|
|
31
|
-
You can pin a version with `npx @milpa/comal@0.1.
|
|
31
|
+
You can pin a version with `npx @milpa/comal@0.1.7` or `npm install --global @milpa/comal@0.1.7`. The local release manifest preserves SHA-256 and SHA-512 integrity is checked against npm at publish time. No independent publisher signature is claimed.
|
|
32
32
|
|
|
33
33
|
## Test without account or model
|
|
34
34
|
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"version": {
|
|
7
|
+
"const": 1
|
|
8
|
+
},
|
|
9
|
+
"scope": {
|
|
10
|
+
"const": "recorded_work_execution"
|
|
11
|
+
},
|
|
12
|
+
"asked": {
|
|
13
|
+
"const": "unjudged"
|
|
14
|
+
},
|
|
15
|
+
"source": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"properties": {
|
|
19
|
+
"session_id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"maxLength": 128
|
|
22
|
+
},
|
|
23
|
+
"row": {
|
|
24
|
+
"type": "integer",
|
|
25
|
+
"minimum": 0
|
|
26
|
+
},
|
|
27
|
+
"sha256": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"maxLength": 64,
|
|
30
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
31
|
+
},
|
|
32
|
+
"integrity": {
|
|
33
|
+
"enum": [
|
|
34
|
+
"verified_hash_chain",
|
|
35
|
+
"legacy",
|
|
36
|
+
"invalid"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"required": [
|
|
41
|
+
"session_id",
|
|
42
|
+
"row",
|
|
43
|
+
"sha256",
|
|
44
|
+
"integrity"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"plan": {
|
|
48
|
+
"anyOf": [
|
|
49
|
+
{
|
|
50
|
+
"type": "null"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"type": "object",
|
|
54
|
+
"additionalProperties": false,
|
|
55
|
+
"properties": {
|
|
56
|
+
"version": {
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"minimum": 1
|
|
59
|
+
},
|
|
60
|
+
"objective": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"maxLength": 1000
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"required": [
|
|
66
|
+
"version",
|
|
67
|
+
"objective"
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"counts": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"properties": {
|
|
76
|
+
"pending": {
|
|
77
|
+
"type": "integer",
|
|
78
|
+
"minimum": 0
|
|
79
|
+
},
|
|
80
|
+
"in_progress": {
|
|
81
|
+
"type": "integer",
|
|
82
|
+
"minimum": 0
|
|
83
|
+
},
|
|
84
|
+
"blocked": {
|
|
85
|
+
"type": "integer",
|
|
86
|
+
"minimum": 0
|
|
87
|
+
},
|
|
88
|
+
"done": {
|
|
89
|
+
"type": "integer",
|
|
90
|
+
"minimum": 0
|
|
91
|
+
},
|
|
92
|
+
"needs_verification": {
|
|
93
|
+
"type": "integer",
|
|
94
|
+
"minimum": 0
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"required": [
|
|
98
|
+
"pending",
|
|
99
|
+
"in_progress",
|
|
100
|
+
"blocked",
|
|
101
|
+
"done",
|
|
102
|
+
"needs_verification"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"tasks_total": {
|
|
106
|
+
"type": "integer",
|
|
107
|
+
"minimum": 0
|
|
108
|
+
},
|
|
109
|
+
"tasks": {
|
|
110
|
+
"type": "array",
|
|
111
|
+
"maxItems": 24,
|
|
112
|
+
"items": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"additionalProperties": false,
|
|
115
|
+
"properties": {
|
|
116
|
+
"id": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"maxLength": 64,
|
|
119
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$"
|
|
120
|
+
},
|
|
121
|
+
"title": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"maxLength": 180
|
|
124
|
+
},
|
|
125
|
+
"criterion": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"maxLength": 400
|
|
128
|
+
},
|
|
129
|
+
"status": {
|
|
130
|
+
"enum": [
|
|
131
|
+
"pending",
|
|
132
|
+
"in_progress",
|
|
133
|
+
"blocked",
|
|
134
|
+
"done",
|
|
135
|
+
"needs_verification"
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"declared_status": {
|
|
139
|
+
"enum": [
|
|
140
|
+
"pending",
|
|
141
|
+
"in_progress",
|
|
142
|
+
"blocked",
|
|
143
|
+
"done",
|
|
144
|
+
"needs_verification"
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
"phase": {
|
|
148
|
+
"enum": [
|
|
149
|
+
"discovery",
|
|
150
|
+
"implementation",
|
|
151
|
+
"verification"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"reason": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"maxLength": 240
|
|
157
|
+
},
|
|
158
|
+
"held_by": {
|
|
159
|
+
"type": "array",
|
|
160
|
+
"maxItems": 24,
|
|
161
|
+
"items": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"maxLength": 64,
|
|
164
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$"
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"checks_total": {
|
|
168
|
+
"type": "integer",
|
|
169
|
+
"minimum": 0
|
|
170
|
+
},
|
|
171
|
+
"checks": {
|
|
172
|
+
"type": "array",
|
|
173
|
+
"maxItems": 4,
|
|
174
|
+
"items": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"additionalProperties": false,
|
|
177
|
+
"properties": {
|
|
178
|
+
"step_id": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"maxLength": 64,
|
|
181
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$"
|
|
182
|
+
},
|
|
183
|
+
"operation": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"maxLength": 128
|
|
186
|
+
},
|
|
187
|
+
"status": {
|
|
188
|
+
"enum": [
|
|
189
|
+
"pending",
|
|
190
|
+
"running",
|
|
191
|
+
"failed",
|
|
192
|
+
"refused",
|
|
193
|
+
"interrupted",
|
|
194
|
+
"not_dispatched",
|
|
195
|
+
"stale",
|
|
196
|
+
"passed"
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
"receipt_id": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"maxLength": 36,
|
|
202
|
+
"pattern": "^[a-f0-9-]{36}$"
|
|
203
|
+
},
|
|
204
|
+
"receipt_row": {
|
|
205
|
+
"type": "integer",
|
|
206
|
+
"minimum": 0
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"required": [
|
|
210
|
+
"step_id",
|
|
211
|
+
"operation",
|
|
212
|
+
"status"
|
|
213
|
+
],
|
|
214
|
+
"dependentRequired": {
|
|
215
|
+
"receipt_id": [
|
|
216
|
+
"receipt_row"
|
|
217
|
+
],
|
|
218
|
+
"receipt_row": [
|
|
219
|
+
"receipt_id"
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
"required": [
|
|
226
|
+
"id",
|
|
227
|
+
"title",
|
|
228
|
+
"criterion",
|
|
229
|
+
"status",
|
|
230
|
+
"declared_status",
|
|
231
|
+
"phase",
|
|
232
|
+
"reason",
|
|
233
|
+
"held_by",
|
|
234
|
+
"checks_total",
|
|
235
|
+
"checks"
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"receipts": {
|
|
240
|
+
"type": "array",
|
|
241
|
+
"maxItems": 8,
|
|
242
|
+
"items": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"additionalProperties": false,
|
|
245
|
+
"properties": {
|
|
246
|
+
"id": {
|
|
247
|
+
"type": "string",
|
|
248
|
+
"maxLength": 36,
|
|
249
|
+
"pattern": "^[a-f0-9-]{36}$"
|
|
250
|
+
},
|
|
251
|
+
"operation": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"maxLength": 128
|
|
254
|
+
},
|
|
255
|
+
"status": {
|
|
256
|
+
"enum": [
|
|
257
|
+
"returned",
|
|
258
|
+
"failed",
|
|
259
|
+
"refused"
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
"row": {
|
|
263
|
+
"type": "integer",
|
|
264
|
+
"minimum": 0
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"required": [
|
|
268
|
+
"id",
|
|
269
|
+
"operation",
|
|
270
|
+
"status",
|
|
271
|
+
"row"
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"required": [
|
|
277
|
+
"version",
|
|
278
|
+
"scope",
|
|
279
|
+
"asked",
|
|
280
|
+
"source",
|
|
281
|
+
"plan",
|
|
282
|
+
"counts",
|
|
283
|
+
"tasks_total",
|
|
284
|
+
"tasks",
|
|
285
|
+
"receipts"
|
|
286
|
+
]
|
|
287
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milpa/comal",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Comal: governed operations, execution receipts, and programming assistance in any language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"backend/verifier/transport.mjs",
|
|
23
23
|
"backend/contracts/artifact-check-v1.schema.json",
|
|
24
24
|
"backend/contracts/remote-commands-v1.json",
|
|
25
|
+
"backend/contracts/remote-workspace-v1.schema.json",
|
|
25
26
|
"governance/chrome-task-v1.json",
|
|
26
27
|
"governance/chrome-task-v2.json",
|
|
27
28
|
"docs",
|
package/src/cli.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { Operation } from './operations.mjs';
|
|
|
37
37
|
import { validateConnectionCandidate } from './connection-validation.mjs';
|
|
38
38
|
import { PluginRuntime, parsePluginCommand } from './plugins.mjs';
|
|
39
39
|
import { registerWorkOperations, workProjection } from './work-plan.mjs';
|
|
40
|
+
import { projectRemoteWorkspace } from './remote-workspace.mjs';
|
|
40
41
|
import { evidenceOperations } from './work-state.mjs';
|
|
41
42
|
import { auditExecution, auditReport } from './audit.mjs';
|
|
42
43
|
import { comalEnvironment } from './compat.mjs';
|
|
@@ -339,7 +340,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal) {
|
|
|
339
340
|
stderr.write(color('90', t("Session {v0} · {v1} calls · {v2} tokens", {v0: event.session_id, v1: event.turns, v2: event.usage.input_tokens + event.usage.output_tokens})) + '\n');
|
|
340
341
|
}
|
|
341
342
|
};
|
|
342
|
-
const house = new House({ cwd, record: event => store.append(event.session_id, event) });
|
|
343
|
+
const house = new House({ cwd, record: async event => { const row=await store.append(event.session_id,event); if(event.session_id===session.id)remoteHost?.invalidateWorkspace(); return row; } });
|
|
343
344
|
if (v.codemode) house.registry.add(codeRunDefinition);
|
|
344
345
|
house.guard('peer-authority',view=>peerGuard.check(view));
|
|
345
346
|
const onPermission = decision => store.append(session.id, { type: 'permission', ...decision });
|
|
@@ -462,7 +463,7 @@ async function boot(args, createScreen, setInterrupt, startupSignal) {
|
|
|
462
463
|
house.registry.add({name:'CloudAccountLogout',description:'Human detaches Remote Control and removes the local Comal account credential.',input_schema:emptyInput,audience:'human',surfaces:['tui'],permission:null,effects:{workspace:'none',external:'write',process:'none',authority:'credential_control'},run:async()=>{await remoteHost?.stop();return cloudClient.logout();}});
|
|
463
464
|
const openRemoteControl=async()=>{
|
|
464
465
|
if(!runtime)throw Error('Remote Control requires an interactive local session.');
|
|
465
|
-
if(!remoteHost || remoteHost.stopped)remoteHost=new RemoteHost({client:new CloudClient({origin:v['cloud-url']}),onInput:(input,binding)=>runtime.admitRemote(input,binding),onStop:id=>runtime.stopRemote(id),onNotice:text=>ui.notice(text)});
|
|
466
|
+
if(!remoteHost || remoteHost.stopped)remoteHost=new RemoteHost({client:new CloudClient({origin:v['cloud-url']}),onInput:(input,binding)=>runtime.admitRemote(input,binding),onStop:id=>runtime.stopRemote(id),onNotice:text=>ui.notice(text),getWorkspace:async snapshot=>projectRemoteWorkspace(snapshot || await store.load(session.id),house.stateContext())});
|
|
466
467
|
const connection = new AbortController(); authController = connection;
|
|
467
468
|
const cancel = () => remoteHost.stop().catch(() => {});
|
|
468
469
|
connection.signal.addEventListener('abort', cancel, { once: true });
|
package/src/remote-control.mjs
CHANGED
|
@@ -26,8 +26,8 @@ const cancelled = () => Object.assign(new Error('Remote Control connection cance
|
|
|
26
26
|
const lifetimeSignal = (run,timeout=10000) => run?.controller ? AbortSignal.any([run.controller.signal,AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
|
|
27
27
|
|
|
28
28
|
export class RemoteHost {
|
|
29
|
-
constructor({client=new CloudClient(),onInput=async()=>{throw Error('Local queue unavailable.');},onStop=async()=>false,onNotice=()=>{},socketFactory=(url,protocol)=>new WebSocket(url,protocol)}={}) {
|
|
30
|
-
Object.assign(this,{client,onInput,onStop,onNotice,socketFactory});this.pending=[];this.turnId=null;this.stopped=true;this.transport='disconnected';this.processing=null;this.sending=null;this.lifecycle=null;this.starting=null;
|
|
29
|
+
constructor({client=new CloudClient(),onInput=async()=>{throw Error('Local queue unavailable.');},onStop=async()=>false,onNotice=()=>{},getWorkspace=null,socketFactory=(url,protocol)=>new WebSocket(url,protocol)}={}) {
|
|
30
|
+
Object.assign(this,{client,onInput,onStop,onNotice,getWorkspace,socketFactory});this.pending=[];this.turnId=null;this.stopped=true;this.transport='disconnected';this.processing=null;this.sending=null;this.lifecycle=null;this.starting=null;
|
|
31
31
|
}
|
|
32
32
|
status(){return this.session?{enabled:!this.stopped,url:this.url,session:this.session.id,transport:this.transport,permissions:'local_terminal_only'}:{enabled:false};}
|
|
33
33
|
current(run,{starting=false}={}) {return this.lifecycle===run && (starting || !this.stopped) && !run?.controller?.signal.aborted;}
|
|
@@ -48,7 +48,8 @@ export class RemoteHost {
|
|
|
48
48
|
const account=await this.client.credentials();this.assertCurrent(run,{starting:true});
|
|
49
49
|
if(!account)throw Error('Sign in with comal cloud login before enabling Remote Control.');
|
|
50
50
|
run.account={origin:account.origin,user_id:account.user_id};run.enablingToken=account.token;
|
|
51
|
-
const
|
|
51
|
+
const workspace=this.getWorkspace?await this.getWorkspace(session):null;this.assertCurrent(run,{starting:true});
|
|
52
|
+
const opened=await this.client.invoke('remote.sessions.open',{local_session_id:session.id,host_id:randomUUID(),label:boundText(session.name || 'Comal',80),features:['commands_v1','thinking_v1',...(this.getWorkspace?['workspace_v1']:[])]},{expectedAccount:run.account,signal:run.controller.signal});
|
|
52
53
|
run.session=opened.session;
|
|
53
54
|
// Abort can race a successful response. Revoke a late-created grant even
|
|
54
55
|
// when the local stop completed before its identifier became available.
|
|
@@ -56,6 +57,7 @@ export class RemoteHost {
|
|
|
56
57
|
this.account=run.account;this.enablingToken=run.enablingToken;this.session=opened.session;this.url=opened.url;this.stopped=false;
|
|
57
58
|
await this.authorize(run);this.assertCurrent(run);await this.pulse(run);this.assertCurrent(run);
|
|
58
59
|
this.transport='https-polling';this.connectSocket(run);
|
|
60
|
+
if(workspace)this.publishWorkspace(workspace,run);
|
|
59
61
|
this.timer=setInterval(()=>this.tick(run),1000);this.timer.unref();
|
|
60
62
|
for(const message of (session.historyMessages || []).slice(-20)){
|
|
61
63
|
const event=message.role==='user'&&(typeof message.content==='string'||Array.isArray(message.content)&&message.content.some(block=>block.type==='text'))?{type:'user',text:typeof message.content==='string'?message.content:message.content.filter(block=>block.type==='text').map(block=>block.text).join('\n')}:message.role==='assistant'?{type:'assistant',message}:null;
|
|
@@ -103,8 +105,30 @@ export class RemoteHost {
|
|
|
103
105
|
}catch{/* HTTPS is already active. A later tick retries WSS. */}
|
|
104
106
|
finally{if(this.connecting===marker){this.connecting=null;if(this.current(run))this.nextSocketAt=Date.now()+10000;}}
|
|
105
107
|
}
|
|
108
|
+
publishWorkspace(workspace,run=this.lifecycle) {
|
|
109
|
+
if(!this.current(run))return;
|
|
110
|
+
const {source,...content}=workspace,key=digest(content);
|
|
111
|
+
if(run.workspaceDigest===key)return;
|
|
112
|
+
if(this.pending.length>=512)throw Error('Remote workspace buffer full.');
|
|
113
|
+
this.pending.push({event_id:randomUUID(),event:{type:'workspace.snapshot',text:'',turn_id:this.turnId||'local',workspace}});
|
|
114
|
+
run.workspaceDigest=key;this.flush(run).catch(()=>{});
|
|
115
|
+
}
|
|
116
|
+
invalidateWorkspace() {
|
|
117
|
+
const run=this.lifecycle;if(!this.current(run)||!this.getWorkspace)return;
|
|
118
|
+
run.workspaceDirty=true;
|
|
119
|
+
if(run.workspaceTimer||run.workspaceLoading)return;
|
|
120
|
+
run.workspaceTimer=setTimeout(()=>{run.workspaceTimer=null;this.refreshWorkspace(run);},750);run.workspaceTimer.unref();
|
|
121
|
+
}
|
|
122
|
+
async refreshWorkspace(run=this.lifecycle) {
|
|
123
|
+
if(!this.current(run)||!this.getWorkspace||run.workspaceLoading)return;
|
|
124
|
+
run.workspaceLoading=true;run.workspaceDirty=false;
|
|
125
|
+
try {const workspace=await this.getWorkspace();if(this.current(run))this.publishWorkspace(workspace,run);}
|
|
126
|
+
catch {if(this.current(run))this.onNotice('Remote work state unavailable; local execution continues.');}
|
|
127
|
+
finally {run.workspaceLoading=false;if(this.current(run)&&run.workspaceDirty)this.invalidateWorkspace();}
|
|
128
|
+
}
|
|
106
129
|
observe(event) {
|
|
107
130
|
if(this.stopped)return;
|
|
131
|
+
if(['tool_result','house_waiting','house_answered','result'].includes(event.type))this.invalidateWorkspace();
|
|
108
132
|
const append=projected=>this.pending.push({event_id:randomUUID(),event:projected});
|
|
109
133
|
if(event.type==='model_stream_progress'&&event.reasoning_chars>0&&!this.thinkingActive){
|
|
110
134
|
this.thinkingActive=true;append({type:'thinking.started',turn_id:this.turnId||'local',text:'Thinking…'});this.flush().catch(()=>{});return;
|
|
@@ -139,6 +163,7 @@ export class RemoteHost {
|
|
|
139
163
|
pulse(run=this.lifecycle){const work=(this.pulsing || Promise.resolve()).catch(()=>{}).then(()=>{if(this.current(run))return this.call('remote.host.pulse',{status:this.turnId?'busy':'idle',turn_id:this.turnId},run);});this.pulsing=work;return work;}
|
|
140
164
|
async beginTurn(){this.turnId=randomUUID();if(!this.stopped)this.pulse().catch(error=>{if(error.code!=='REMOTE_CANCELLED')this.onNotice('Remote state unavailable: '+error.message);});}
|
|
141
165
|
async endTurn(){
|
|
166
|
+
this.invalidateWorkspace();
|
|
142
167
|
if(!this.stopped){this.pending.push({event_id:randomUUID(),event:{type:'turn.settled',text:'Local turn settled.',turn_id:this.turnId || 'local'}});this.flush().catch(()=>{});}
|
|
143
168
|
clearTimeout(this.deltaTimer);this.deltaTimer=null;this.delta='';this.thinkingActive=false;this.turnId=null;this.pulse().catch(()=>{});
|
|
144
169
|
}
|
|
@@ -172,7 +197,7 @@ export class RemoteHost {
|
|
|
172
197
|
}
|
|
173
198
|
async stop(){
|
|
174
199
|
const run=this.lifecycle || {session:this.session,account:this.account,enablingToken:this.enablingToken};
|
|
175
|
-
this.lifecycle=null;this.starting=null;run.controller?.abort();
|
|
200
|
+
this.lifecycle=null;this.starting=null;run.controller?.abort();clearTimeout(run.workspaceTimer);
|
|
176
201
|
this.stopped=true;clearInterval(this.timer);clearTimeout(this.deltaTimer);clearTimeout(this.socketTimeout);this.socket?.close();this.socket=null;this.delta='';this.transport='disconnected';
|
|
177
202
|
this.access=null;this.enablingToken=null;this.pending=[];this.turnId=null;this.processing=null;this.sending=null;this.pulsing=null;this.connecting=null;
|
|
178
203
|
await this.revoke(run);return this.status();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { foldWorkBook } from './work-plan.mjs';
|
|
2
|
+
import { auditJournal } from './audit.mjs';
|
|
3
|
+
|
|
4
|
+
const text = (value, bytes) => Buffer.from(String(value ?? '')).subarray(0, bytes).toString('utf8').replace(/\uFFFD$/, '');
|
|
5
|
+
const reasons = {
|
|
6
|
+
'Una comprobación perdió vigencia en el libro.':'A recorded check is no longer current.',
|
|
7
|
+
'La evidencia de una dependencia cambió; vuelve a comprobar esta tarea.':'Dependency evidence changed; verify this task again.',
|
|
8
|
+
'Una dependencia perdió su cierre vigente.':'A dependency no longer has current completion evidence.',
|
|
9
|
+
'Espera una respuesta humana.':'Waiting for an answer in the local terminal.',
|
|
10
|
+
};
|
|
11
|
+
const states = new Set(['pending', 'in_progress', 'blocked', 'done', 'needs_verification']);
|
|
12
|
+
const checkStates = new Set(['running', 'failed', 'refused', 'interrupted', 'not_dispatched']);
|
|
13
|
+
|
|
14
|
+
/** Host projection only: no arguments, assertions, file contents, drafts or raw outputs. */
|
|
15
|
+
export function projectRemoteWorkspace(session, context = {}) {
|
|
16
|
+
const rows = session.rows || [], journal = auditJournal(rows), book = foldWorkBook(session, context);
|
|
17
|
+
const counts = Object.fromEntries([...states].map(status => [status, book.tasks.filter(task => task.status === status).length]));
|
|
18
|
+
const tasks = [...book.tasks].sort((a, b) => ['in_progress', 'blocked', 'needs_verification', 'pending', 'done'].indexOf(a.status) - ['in_progress', 'blocked', 'needs_verification', 'pending', 'done'].indexOf(b.status)).slice(0, 24).map(task => ({
|
|
19
|
+
id: task.id, title: text(task.title, 180), criterion: text(task.criterion, 400),
|
|
20
|
+
status: journal.verified ? task.status : task.status === 'done' ? 'needs_verification' : task.status,
|
|
21
|
+
declared_status: states.has(task.declared_status) ? task.declared_status : 'pending',
|
|
22
|
+
phase: task.phase || 'implementation', reason: text(reasons[task.reason] || task.reason, 240), held_by: task.held_by.slice(0, 24),
|
|
23
|
+
checks_total: task.checks.length,
|
|
24
|
+
checks: task.checks.slice(0, 4).map(check => {
|
|
25
|
+
const step = book.steps.find(step => step.task_id === task.id && step.step_id === check.step_id);
|
|
26
|
+
const receipt = book.receipts.get(step?.receipt_id);
|
|
27
|
+
const status = journal.verified && book.checkValid(task, check, step) ? 'passed'
|
|
28
|
+
: step?.preflight_failure ? 'not_dispatched'
|
|
29
|
+
: step?.status === 'returned' ? 'stale'
|
|
30
|
+
: checkStates.has(step?.status) ? step.status : 'pending';
|
|
31
|
+
return { step_id: check.step_id, operation: text(check.operation, 128), status,
|
|
32
|
+
...(receipt ? { receipt_id: receipt.invocation_id, receipt_row: receipt.evidence_row } : {}) };
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
const references = new Set([...book.steps.map(step => step.receipt_id), ...book.tasks.flatMap(task => task.receipt_ids || [])]);
|
|
36
|
+
const receipts = [...book.receipts.values()].filter(receipt => references.has(receipt.invocation_id)).slice(-8).reverse().map(receipt => ({
|
|
37
|
+
id: receipt.invocation_id, operation: text(receipt.operation, 128), status: receipt.status, row: receipt.evidence_row,
|
|
38
|
+
}));
|
|
39
|
+
if (!journal.verified) { const previousDone=counts.done; counts.done=0; counts.needs_verification+=previousDone; }
|
|
40
|
+
const result = {
|
|
41
|
+
version: 1, scope: 'recorded_work_execution', asked: 'unjudged',
|
|
42
|
+
source: { session_id: session.id, row: Math.max(0, rows.length - 1), sha256: journal.tip_sha256 || '0'.repeat(64), integrity: journal.verified ? 'verified_hash_chain' : journal.consistent ? 'legacy' : 'invalid' },
|
|
43
|
+
plan: book.plan ? { version: book.plan.version, objective: text(book.plan.objective, 1000) } : null,
|
|
44
|
+
counts, tasks_total: book.tasks.length, tasks, receipts,
|
|
45
|
+
};
|
|
46
|
+
// The stream has a 48 KiB event ceiling, including its transport envelope.
|
|
47
|
+
// Keep whole task/check records and disclose omissions through total counts.
|
|
48
|
+
while (Buffer.byteLength(JSON.stringify(result)) > 36000 && result.tasks.length) result.tasks.pop();
|
|
49
|
+
return result;
|
|
50
|
+
}
|