@agimon-ai/doompi-workflow 0.0.1-alpha.39 → 0.0.1-alpha.40
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 -2
- package/dist/adapters/workflowsHubChannel.cjs +1 -1
- package/dist/adapters/workflowsHubChannel.cjs.map +1 -1
- package/dist/adapters/workflowsHubChannel.d.cts.map +1 -1
- package/dist/adapters/workflowsHubChannel.d.mts.map +1 -1
- package/dist/adapters/workflowsHubChannel.mjs +1 -1
- package/dist/adapters/workflowsHubChannel.mjs.map +1 -1
- package/dist/services/workflowRuns.cjs +2 -2
- package/dist/services/workflowRuns.cjs.map +1 -1
- package/dist/services/workflowRuns.d.cts +0 -2
- package/dist/services/workflowRuns.d.cts.map +1 -1
- package/dist/services/workflowRuns.d.mts +0 -2
- package/dist/services/workflowRuns.d.mts.map +1 -1
- package/dist/services/workflowRuns.mjs +2 -2
- package/dist/services/workflowRuns.mjs.map +1 -1
- package/package.json +7 -6
- package/web/LaunchWorkflowDialog.tsx +22 -10
- package/web/WorkflowCatalogDrawer.tsx +1 -1
- package/web/WorkflowsPanel.tsx +7 -5
- package/web/terminalApi.ts +9 -4
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# @agimon-ai/doompi-workflow
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
for DoomPi.
|
|
3
|
+
Discover, launch, monitor, control, and recover asynchronous DoomPi workflows.
|
|
5
4
|
|
|
6
5
|
Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
|
|
7
6
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../types/webWorkflows.cjs"),t=require("./workflowCatalogChannel.cjs"),n=require("../services/workflowRuns.cjs"),r=require("./workflowWatcher.cjs");function i(t={}){return{frameType:e.WORKFLOW_RUNS_TYPE,start(e){let i=[],a=new Map,o=e=>n.presentWorkflowRuns(i.filter(t=>n.runBelongsToSession(t,
|
|
1
|
+
const e=require("../types/webWorkflows.cjs"),t=require("./workflowCatalogChannel.cjs"),n=require("../services/workflowRuns.cjs"),r=require("./workflowWatcher.cjs");function i(t={}){return{frameType:e.WORKFLOW_RUNS_TYPE,start(e){let i=[],a=new Map,o=e=>n.presentWorkflowRuns(i.filter(t=>n.runBelongsToSession(t,e.sessionId)).map(e=>e.view),Date.now()),s=(t.watch??r.watchWorkflowRuns)(t=>{i=t;for(let t of e.sessions()){let n=o(t),r=JSON.stringify(n);r!==a.get(t.sessionId)&&(a.set(t.sessionId,r),e.publish(t.sessionId,{runs:n}))}});return{payloadFor(e){return{runs:o(e)}},sessionRemoved(e){a.delete(e)},close(){s.close()}}}}}const a=[i(),t.createWorkflowCatalogChannel()];exports.createWorkflowsChannel=i,exports.webHubChannels=a;
|
|
2
2
|
//# sourceMappingURL=workflowsHubChannel.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowsHubChannel.cjs","names":["WORKFLOW_RUNS_TYPE","presentWorkflowRuns","runBelongsToSession","watchWorkflowRuns","createWorkflowCatalogChannel"],"sources":["../../src/adapters/workflowsHubChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type ParsedWorkflowRun, presentWorkflowRuns, runBelongsToSession } from '../services/workflowRuns.ts';\nimport { WORKFLOW_RUNS_TYPE, type WorkflowRunView } from '../types/webWorkflows.ts';\nimport { createWorkflowCatalogChannel } from './workflowCatalogChannel.ts';\nimport { watchWorkflowRuns } from './workflowWatcher.ts';\n\nexport interface WorkflowsChannelOptions {\n /** Injectable for tests; defaults to watching workflow-mcp's registry. */\n watch?: typeof watchWorkflowRuns;\n}\n\n/**\n * The workflows data channel: one hub-wide registry watcher, filtered and\n * presented per session on demand, published as { runs } payloads under the\n * 'workflow_runs' frame type. Live announcements are deduped per session by\n * JSON fingerprint; the subscribe-time snapshot is recomputed so retention\n * keeps moving between registry changes.\n */\nexport function createWorkflowsChannel(options: WorkflowsChannelOptions = {}): WebHubChannel {\n return {\n frameType: WORKFLOW_RUNS_TYPE,\n start(host) {\n let parsed: ParsedWorkflowRun[] = [];\n const lastPublished = new Map<string, string>();\n const runsFor = (scope: HubSessionScope): WorkflowRunView[] =>\n presentWorkflowRuns(\n parsed
|
|
1
|
+
{"version":3,"file":"workflowsHubChannel.cjs","names":["WORKFLOW_RUNS_TYPE","presentWorkflowRuns","runBelongsToSession","watchWorkflowRuns","createWorkflowCatalogChannel"],"sources":["../../src/adapters/workflowsHubChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type ParsedWorkflowRun, presentWorkflowRuns, runBelongsToSession } from '../services/workflowRuns.ts';\nimport { WORKFLOW_RUNS_TYPE, type WorkflowRunView } from '../types/webWorkflows.ts';\nimport { createWorkflowCatalogChannel } from './workflowCatalogChannel.ts';\nimport { watchWorkflowRuns } from './workflowWatcher.ts';\n\nexport interface WorkflowsChannelOptions {\n /** Injectable for tests; defaults to watching workflow-mcp's registry. */\n watch?: typeof watchWorkflowRuns;\n}\n\n/**\n * The workflows data channel: one hub-wide registry watcher, filtered and\n * presented per session on demand, published as { runs } payloads under the\n * 'workflow_runs' frame type. Live announcements are deduped per session by\n * JSON fingerprint; the subscribe-time snapshot is recomputed so retention\n * keeps moving between registry changes.\n */\nexport function createWorkflowsChannel(options: WorkflowsChannelOptions = {}): WebHubChannel {\n return {\n frameType: WORKFLOW_RUNS_TYPE,\n start(host) {\n let parsed: ParsedWorkflowRun[] = [];\n const lastPublished = new Map<string, string>();\n const runsFor = (scope: HubSessionScope): WorkflowRunView[] =>\n presentWorkflowRuns(\n parsed.filter((run) => runBelongsToSession(run, scope.sessionId)).map((run) => run.view),\n Date.now(),\n );\n const watcher = (options.watch ?? watchWorkflowRuns)((runs) => {\n parsed = runs;\n for (const scope of host.sessions()) {\n const view = runsFor(scope);\n const json = JSON.stringify(view);\n if (json === lastPublished.get(scope.sessionId)) continue;\n lastPublished.set(scope.sessionId, json);\n host.publish(scope.sessionId, { runs: view });\n }\n });\n const channelSource: HubChannelSource = {\n payloadFor(scope) {\n return { runs: runsFor(scope) };\n },\n sessionRemoved(sessionId) {\n lastPublished.delete(sessionId);\n },\n close() {\n watcher.close();\n },\n };\n return channelSource;\n },\n };\n}\n\n/** The named export the generated hub registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createWorkflowsChannel(), createWorkflowCatalogChannel()];\n"],"mappings":"oKAkBA,SAAgB,EAAuB,EAAmC,CAAC,EAAkB,CAC3F,MAAO,CACL,UAAWA,EAAAA,mBACX,MAAM,EAAM,CACV,IAAI,EAA8B,CAAC,EAC7B,EAAgB,IAAI,IACpB,EAAW,GACfC,EAAAA,oBACE,EAAO,OAAQ,GAAQC,EAAAA,oBAAoB,EAAK,EAAM,SAAS,CAAC,CAAC,CAAC,IAAK,GAAQ,EAAI,IAAI,EACvF,KAAK,IAAI,CACX,EACI,GAAW,EAAQ,OAASC,EAAAA,kBAAAA,CAAoB,GAAS,CAC7D,EAAS,EACT,IAAK,IAAM,KAAS,EAAK,SAAS,EAAG,CACnC,IAAM,EAAO,EAAQ,CAAK,EACpB,EAAO,KAAK,UAAU,CAAI,EAC5B,IAAS,EAAc,IAAI,EAAM,SAAS,IAC9C,EAAc,IAAI,EAAM,UAAW,CAAI,EACvC,EAAK,QAAQ,EAAM,UAAW,CAAE,KAAM,CAAK,CAAC,EAC9C,CACF,CAAC,EAYD,MAAO,CAVL,WAAW,EAAO,CAChB,MAAO,CAAE,KAAM,EAAQ,CAAK,CAAE,CAChC,EACA,eAAe,EAAW,CACxB,EAAc,OAAO,CAAS,CAChC,EACA,OAAQ,CACN,EAAQ,MAAM,CAChB,CAEiB,CACrB,CACF,CACF,CAGA,MAAa,EAA2C,CAAC,EAAuB,EAAGC,EAAAA,6BAA6B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowsHubChannel.d.cts","names":[],"sources":["../../src/adapters/workflowsHubChannel.ts"],"mappings":";;;UAMiB;;EAEf,eAAe;;;;;;;;;iBAUD,uBAAuB,UAAS,0BAA+B;;
|
|
1
|
+
{"version":3,"file":"workflowsHubChannel.d.cts","names":[],"sources":["../../src/adapters/workflowsHubChannel.ts"],"mappings":";;;UAMiB;;EAEf,eAAe;;;;;;;;;iBAUD,uBAAuB,UAAS,0BAA+B;;cAsClE,yBAAyB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowsHubChannel.d.mts","names":[],"sources":["../../src/adapters/workflowsHubChannel.ts"],"mappings":";;;UAMiB;;EAEf,eAAe;;;;;;;;;iBAUD,uBAAuB,UAAS,0BAA+B;;
|
|
1
|
+
{"version":3,"file":"workflowsHubChannel.d.mts","names":[],"sources":["../../src/adapters/workflowsHubChannel.ts"],"mappings":";;;UAMiB;;EAEf,eAAe;;;;;;;;;iBAUD,uBAAuB,UAAS,0BAA+B;;cAsClE,yBAAyB"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{WORKFLOW_RUNS_TYPE as e}from"../types/webWorkflows.mjs";import{createWorkflowCatalogChannel as t}from"./workflowCatalogChannel.mjs";import{presentWorkflowRuns as n,runBelongsToSession as r}from"../services/workflowRuns.mjs";import{watchWorkflowRuns as i}from"./workflowWatcher.mjs";function a(t={}){return{frameType:e,start(e){let a=[],o=new Map,s=e=>n(a.filter(t=>r(t,
|
|
1
|
+
import{WORKFLOW_RUNS_TYPE as e}from"../types/webWorkflows.mjs";import{createWorkflowCatalogChannel as t}from"./workflowCatalogChannel.mjs";import{presentWorkflowRuns as n,runBelongsToSession as r}from"../services/workflowRuns.mjs";import{watchWorkflowRuns as i}from"./workflowWatcher.mjs";function a(t={}){return{frameType:e,start(e){let a=[],o=new Map,s=e=>n(a.filter(t=>r(t,e.sessionId)).map(e=>e.view),Date.now()),c=(t.watch??i)(t=>{a=t;for(let t of e.sessions()){let n=s(t),r=JSON.stringify(n);r!==o.get(t.sessionId)&&(o.set(t.sessionId,r),e.publish(t.sessionId,{runs:n}))}});return{payloadFor(e){return{runs:s(e)}},sessionRemoved(e){o.delete(e)},close(){c.close()}}}}}const o=[a(),t()];export{a as createWorkflowsChannel,o as webHubChannels};
|
|
2
2
|
//# sourceMappingURL=workflowsHubChannel.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowsHubChannel.mjs","names":[],"sources":["../../src/adapters/workflowsHubChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type ParsedWorkflowRun, presentWorkflowRuns, runBelongsToSession } from '../services/workflowRuns.ts';\nimport { WORKFLOW_RUNS_TYPE, type WorkflowRunView } from '../types/webWorkflows.ts';\nimport { createWorkflowCatalogChannel } from './workflowCatalogChannel.ts';\nimport { watchWorkflowRuns } from './workflowWatcher.ts';\n\nexport interface WorkflowsChannelOptions {\n /** Injectable for tests; defaults to watching workflow-mcp's registry. */\n watch?: typeof watchWorkflowRuns;\n}\n\n/**\n * The workflows data channel: one hub-wide registry watcher, filtered and\n * presented per session on demand, published as { runs } payloads under the\n * 'workflow_runs' frame type. Live announcements are deduped per session by\n * JSON fingerprint; the subscribe-time snapshot is recomputed so retention\n * keeps moving between registry changes.\n */\nexport function createWorkflowsChannel(options: WorkflowsChannelOptions = {}): WebHubChannel {\n return {\n frameType: WORKFLOW_RUNS_TYPE,\n start(host) {\n let parsed: ParsedWorkflowRun[] = [];\n const lastPublished = new Map<string, string>();\n const runsFor = (scope: HubSessionScope): WorkflowRunView[] =>\n presentWorkflowRuns(\n parsed
|
|
1
|
+
{"version":3,"file":"workflowsHubChannel.mjs","names":[],"sources":["../../src/adapters/workflowsHubChannel.ts"],"sourcesContent":["import type { HubChannelSource, HubSessionScope, WebHubChannel } from '@agimon-ai/doompi-web-contracts';\nimport { type ParsedWorkflowRun, presentWorkflowRuns, runBelongsToSession } from '../services/workflowRuns.ts';\nimport { WORKFLOW_RUNS_TYPE, type WorkflowRunView } from '../types/webWorkflows.ts';\nimport { createWorkflowCatalogChannel } from './workflowCatalogChannel.ts';\nimport { watchWorkflowRuns } from './workflowWatcher.ts';\n\nexport interface WorkflowsChannelOptions {\n /** Injectable for tests; defaults to watching workflow-mcp's registry. */\n watch?: typeof watchWorkflowRuns;\n}\n\n/**\n * The workflows data channel: one hub-wide registry watcher, filtered and\n * presented per session on demand, published as { runs } payloads under the\n * 'workflow_runs' frame type. Live announcements are deduped per session by\n * JSON fingerprint; the subscribe-time snapshot is recomputed so retention\n * keeps moving between registry changes.\n */\nexport function createWorkflowsChannel(options: WorkflowsChannelOptions = {}): WebHubChannel {\n return {\n frameType: WORKFLOW_RUNS_TYPE,\n start(host) {\n let parsed: ParsedWorkflowRun[] = [];\n const lastPublished = new Map<string, string>();\n const runsFor = (scope: HubSessionScope): WorkflowRunView[] =>\n presentWorkflowRuns(\n parsed.filter((run) => runBelongsToSession(run, scope.sessionId)).map((run) => run.view),\n Date.now(),\n );\n const watcher = (options.watch ?? watchWorkflowRuns)((runs) => {\n parsed = runs;\n for (const scope of host.sessions()) {\n const view = runsFor(scope);\n const json = JSON.stringify(view);\n if (json === lastPublished.get(scope.sessionId)) continue;\n lastPublished.set(scope.sessionId, json);\n host.publish(scope.sessionId, { runs: view });\n }\n });\n const channelSource: HubChannelSource = {\n payloadFor(scope) {\n return { runs: runsFor(scope) };\n },\n sessionRemoved(sessionId) {\n lastPublished.delete(sessionId);\n },\n close() {\n watcher.close();\n },\n };\n return channelSource;\n },\n };\n}\n\n/** The named export the generated hub registry imports. */\nexport const webHubChannels: readonly WebHubChannel[] = [createWorkflowsChannel(), createWorkflowCatalogChannel()];\n"],"mappings":"iSAkBA,SAAgB,EAAuB,EAAmC,CAAC,EAAkB,CAC3F,MAAO,CACL,UAAW,EACX,MAAM,EAAM,CACV,IAAI,EAA8B,CAAC,EAC7B,EAAgB,IAAI,IACpB,EAAW,GACf,EACE,EAAO,OAAQ,GAAQ,EAAoB,EAAK,EAAM,SAAS,CAAC,CAAC,CAAC,IAAK,GAAQ,EAAI,IAAI,EACvF,KAAK,IAAI,CACX,EACI,GAAW,EAAQ,OAAS,EAAA,CAAoB,GAAS,CAC7D,EAAS,EACT,IAAK,IAAM,KAAS,EAAK,SAAS,EAAG,CACnC,IAAM,EAAO,EAAQ,CAAK,EACpB,EAAO,KAAK,UAAU,CAAI,EAC5B,IAAS,EAAc,IAAI,EAAM,SAAS,IAC9C,EAAc,IAAI,EAAM,UAAW,CAAI,EACvC,EAAK,QAAQ,EAAM,UAAW,CAAE,KAAM,CAAK,CAAC,EAC9C,CACF,CAAC,EAYD,MAAO,CAVL,WAAW,EAAO,CAChB,MAAO,CAAE,KAAM,EAAQ,CAAK,CAAE,CAChC,EACA,eAAe,EAAW,CACxB,EAAc,OAAO,CAAS,CAChC,EACA,OAAQ,CACN,EAAQ,MAAM,CAChB,CAEiB,CACrB,CACF,CACF,CAGA,MAAa,EAA2C,CAAC,EAAuB,EAAG,EAA6B,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
let e=require("node:path");const t=[`running`,`completed`,`error`],n=864e5,r=`PI_SESSION_ID`,i=new Set([`running`,`completed`,`skipped`,`failed`,`pause_requested`,`paused`,`resumed`]),a=new Set([`success`,`skipped`,`failed`,`interrupted`]),o=new Set([`running`,`pause_requested`,`paused`,`resume_requested`]),s=new Set([`completed`,`skipped`,`failed`]),c=`post`,l=/\u001b\[[0-9;]*[A-Za-z]/g;function u(t){return(0,e.resolve)(t.envValue!==void 0&&t.envValue!==``?t.envValue:(0,e.resolve)(t.homeDir,`.workflow-mcp`))}function d(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function f(e){return typeof e==`string`&&e!==``?e:void 0}function p(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function m(e){return e.replace(l,``).trim()}function h(e){let n;try{n=JSON.parse(e)}catch{return}if(!d(n))return;let i=f(n.runKey),s=f(n.workspace),c=f(n.workflowPath),l=f(n.startedAt),u=f(n.stage);if(!i||!s||!c||!l||u===void 0||!t.includes(u))return;let p=f(n.outcome),h=f(n.executionState),g=f(n.errorMessage),_=d(n.env)?n.env:void 0,v={runKey:i,workspace:s,displayName:f(n.displayName)??i,...f(n.workflowName)===void 0?{}:{workflowName:f(n.workflowName)},workflowPath:c,stage:u,...p!==void 0&&a.has(p)?{outcome:p}:{},...h!==void 0&&o.has(h)?{executionState:h}:{},...f(n.prompt)===void 0?{}:{prompt:f(n.prompt)},startedAt:l,...f(n.finishedAt)===void 0?{}:{finishedAt:f(n.finishedAt)},...g===void 0?{}:{errorMessage:m(g)},...f(n.failedJob)===void 0?{}:{failedJob:f(n.failedJob)},...n.stale===!0?{stale:!0}:{},...f(n.staleReason)===void 0?{}:{staleReason:f(n.staleReason)},...f(n.worktreeBranch)===void 0?{}:{worktreeBranch:f(n.worktreeBranch)},jobs:[]},y=_===void 0?void 0:f(_[r])
|
|
2
|
-
`)){let e=n.trim();if(e===``)continue;let r;try{r=JSON.parse(e)}catch{continue}if(!d(r))continue;let a=r.type,o=f(r.status),s=f(r.job),c=f(r.at);if(a!==`job`&&a!==`step`||!s||!c||o===void 0||!i.has(o))continue;let l=f(r.step),u=p(r.index),m=p(r.total),h=f(r.reason);t.push({type:a,status:o,job:s,...l===void 0?{}:{step:l},...u===void 0?{}:{index:u},...m===void 0?{}:{total:m},...h===void 0?{}:{reason:h},at:c})}return t}function _(e){return e===`pre`?`pre`:e===c?c:`job`}function v(e){let t=[],n=new Map;for(let r of e){let e=n.get(r.job);if(e===void 0&&(e={name:r.job,phase:_(r.job),status:r.status,steps:[]},n.set(r.job,e),t.push(e)),r.type===`job`){e.status=r.status,r.reason!==void 0&&(e.reason=r.reason),r.index!==void 0&&(e.index=r.index),r.total!==void 0&&(e.total=r.total),r.status===`running`&&e.startedAt===void 0&&(e.startedAt=r.at),s.has(r.status)&&(e.endedAt=r.at);continue}if(r.step===void 0)continue;let i=e.steps.find(e=>e.name===r.step);i===void 0&&(i={name:r.step,status:r.status},e.steps.push(i)),i.status=r.status,r.reason!==void 0&&(i.reason=r.reason),r.status===`running`&&i.startedAt===void 0&&(i.startedAt=r.at),s.has(r.status)&&(i.endedAt=r.at)}return t}const y=new Set([`running`,`pause_requested`,`paused`,`resumed`]);function b(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(y.has(n.status)){for(let e=n.steps.length-1;e>=0;--e){let t=n.steps[e];if(y.has(t.status))return{job:n.name,step:t.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}return{job:n.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}}}function x(e,t){let n=e.stage===`running`?b(t):void 0;return{...e,jobs:t,...n===void 0?{}:{position:n}}}function S(e,t){return e
|
|
1
|
+
let e=require("node:path");const t=[`running`,`completed`,`error`],n=864e5,r=`PI_SESSION_ID`,i=new Set([`running`,`completed`,`skipped`,`failed`,`pause_requested`,`paused`,`resumed`]),a=new Set([`success`,`skipped`,`failed`,`interrupted`]),o=new Set([`running`,`pause_requested`,`paused`,`resume_requested`]),s=new Set([`completed`,`skipped`,`failed`]),c=`post`,l=/\u001b\[[0-9;]*[A-Za-z]/g;function u(t){return(0,e.resolve)(t.envValue!==void 0&&t.envValue!==``?t.envValue:(0,e.resolve)(t.homeDir,`.workflow-mcp`))}function d(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function f(e){return typeof e==`string`&&e!==``?e:void 0}function p(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function m(e){return e.replace(l,``).trim()}function h(e){let n;try{n=JSON.parse(e)}catch{return}if(!d(n))return;let i=f(n.runKey),s=f(n.workspace),c=f(n.workflowPath),l=f(n.startedAt),u=f(n.stage);if(!i||!s||!c||!l||u===void 0||!t.includes(u))return;let p=f(n.outcome),h=f(n.executionState),g=f(n.errorMessage),_=d(n.env)?n.env:void 0,v={runKey:i,workspace:s,displayName:f(n.displayName)??i,...f(n.workflowName)===void 0?{}:{workflowName:f(n.workflowName)},workflowPath:c,stage:u,...p!==void 0&&a.has(p)?{outcome:p}:{},...h!==void 0&&o.has(h)?{executionState:h}:{},...f(n.prompt)===void 0?{}:{prompt:f(n.prompt)},startedAt:l,...f(n.finishedAt)===void 0?{}:{finishedAt:f(n.finishedAt)},...g===void 0?{}:{errorMessage:m(g)},...f(n.failedJob)===void 0?{}:{failedJob:f(n.failedJob)},...n.stale===!0?{stale:!0}:{},...f(n.staleReason)===void 0?{}:{staleReason:f(n.staleReason)},...f(n.worktreeBranch)===void 0?{}:{worktreeBranch:f(n.worktreeBranch)},jobs:[]},y=_===void 0?void 0:f(_[r]);return{view:v,...y===void 0?{}:{piSessionId:y}}}function g(e){let t=[];for(let n of e.split(`
|
|
2
|
+
`)){let e=n.trim();if(e===``)continue;let r;try{r=JSON.parse(e)}catch{continue}if(!d(r))continue;let a=r.type,o=f(r.status),s=f(r.job),c=f(r.at);if(a!==`job`&&a!==`step`||!s||!c||o===void 0||!i.has(o))continue;let l=f(r.step),u=p(r.index),m=p(r.total),h=f(r.reason);t.push({type:a,status:o,job:s,...l===void 0?{}:{step:l},...u===void 0?{}:{index:u},...m===void 0?{}:{total:m},...h===void 0?{}:{reason:h},at:c})}return t}function _(e){return e===`pre`?`pre`:e===c?c:`job`}function v(e){let t=[],n=new Map;for(let r of e){let e=n.get(r.job);if(e===void 0&&(e={name:r.job,phase:_(r.job),status:r.status,steps:[]},n.set(r.job,e),t.push(e)),r.type===`job`){e.status=r.status,r.reason!==void 0&&(e.reason=r.reason),r.index!==void 0&&(e.index=r.index),r.total!==void 0&&(e.total=r.total),r.status===`running`&&e.startedAt===void 0&&(e.startedAt=r.at),s.has(r.status)&&(e.endedAt=r.at);continue}if(r.step===void 0)continue;let i=e.steps.find(e=>e.name===r.step);i===void 0&&(i={name:r.step,status:r.status},e.steps.push(i)),i.status=r.status,r.reason!==void 0&&(i.reason=r.reason),r.status===`running`&&i.startedAt===void 0&&(i.startedAt=r.at),s.has(r.status)&&(i.endedAt=r.at)}return t}const y=new Set([`running`,`pause_requested`,`paused`,`resumed`]);function b(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(y.has(n.status)){for(let e=n.steps.length-1;e>=0;--e){let t=n.steps[e];if(y.has(t.status))return{job:n.name,step:t.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}return{job:n.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}}}function x(e,t){let n=e.stage===`running`?b(t):void 0;return{...e,jobs:t,...n===void 0?{}:{position:n}}}function S(e,t){return e.piSessionId!==void 0&&e.piSessionId===t}function C(e){if(e===void 0)return 0;let t=Date.parse(e);return Number.isFinite(t)?t:0}const w={running:0,error:1,completed:2};function T(e,t){return e.stage!==`error`||t-(C(e.finishedAt)||C(e.startedAt))<n}function E(e){return e.stage===`running`?C(e.startedAt):C(e.finishedAt)||C(e.startedAt)}function D(e,t){let n=new Map;for(let r of e){if(!T(r,t))continue;let e=n.get(r.stage);e?e.push(r):n.set(r.stage,[r])}return[...n.entries()].sort(([e],[t])=>w[e]-w[t]).flatMap(([,e])=>e.sort((e,t)=>E(t)-E(e)).slice(0,24))}exports.PI_SESSION_ENV=r,exports.PROGRESS_FILE_NAME=`progress.ndjson`,exports.RUN_RECORD_FILE_NAME=`run.json`,exports.WORKFLOW_ERROR_RETENTION_MS=n,exports.WORKFLOW_HOME_ENV=`WORKFLOW_MCP_HOME`,exports.WORKFLOW_STAGES=t,exports.WORKSPACES_DIR_NAME=`workspaces`,exports.completeWorkflowRunView=x,exports.foldWorkflowProgress=v,exports.parseWorkflowProgress=g,exports.parseWorkflowRunRecord=h,exports.presentWorkflowRuns=D,exports.resolveWorkflowHome=u,exports.runBelongsToSession=S,exports.workflowPosition=b;
|
|
3
3
|
//# sourceMappingURL=workflowRuns.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowRuns.cjs","names":["resolve"],"sources":["../../src/services/workflowRuns.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type {\n WorkflowJobPhase,\n WorkflowJobView,\n WorkflowOutcome,\n WorkflowPosition,\n WorkflowProgressState,\n WorkflowRunView,\n WorkflowStage,\n WorkflowStepView,\n} from '../types/webWorkflows.ts';\n\n/**\n * MIRRORS @agimon-ai/workflow-mcp's on-disk registry\n * (src/services/WorkflowRegistryService.ts and WorkflowProgressService.ts in\n * that package). The engine keeps one directory per run under\n * `<home>/workspaces/<workspace>/<stage>/<runKey>/` holding a `run.json`\n * record and an append-only `progress.ndjson` job/step log; the hub only ever\n * reads them. The derivation is duplicated rather than imported because\n * doompi-web must not depend on the engine at runtime; the unit suite pins\n * this mirror against the package's published types.\n */\nconst DEFAULT_WORKFLOW_HOME_DIR_NAME = '.workflow-mcp';\nexport const WORKFLOW_HOME_ENV = 'WORKFLOW_MCP_HOME';\nexport const WORKSPACES_DIR_NAME = 'workspaces';\nexport const RUN_RECORD_FILE_NAME = 'run.json';\nexport const PROGRESS_FILE_NAME = 'progress.ndjson';\nexport const WORKFLOW_STAGES: readonly WorkflowStage[] = ['running', 'completed', 'error'];\n\n/**\n * Errored runs stay a day, which is how long recovery has to act on them.\n *\n * Finished runs have no retention of their own: a session keeps every run it\n * started, grouped by outcome, for as long as the session lives. A run leaves\n * the view when the engine prunes its registry directory, not on a timer here,\n * because \"what did this session run today\" is a question asked long after the\n * run settled.\n */\nexport const WORKFLOW_ERROR_RETENTION_MS = 24 * 60 * 60 * 1000;\n/**\n * Upper bound per outcome group rather than over the whole list.\n *\n * One cap across every run let a long stream of green runs push a failure out\n * of the list, which is the one thing that must never fall off the end.\n */\nexport const MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP = 24;\n\n/** The env key doompi-workflow stamps on runs it launches; ties a run to a Pi session. */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\nconst PROGRESS_STATES: ReadonlySet<string> = new Set([\n 'running',\n 'completed',\n 'skipped',\n 'failed',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\nconst OUTCOMES: ReadonlySet<string> = new Set(['success', 'skipped', 'failed', 'interrupted']);\nconst EXECUTION_STATES: ReadonlySet<string> = new Set(['running', 'pause_requested', 'paused', 'resume_requested']);\nconst STEP_TERMINAL_STATES: ReadonlySet<WorkflowProgressState> = new Set(['completed', 'skipped', 'failed']);\n/** The engine records its pre:/post: blocks under these reserved pseudo-job names. */\nconst PHASE_JOB_PRE = 'pre';\nconst PHASE_JOB_POST = 'post';\n\n// oxlint-disable-next-line no-control-regex -- stripping terminal color codes from errorMessage is the point\nconst ANSI_ESCAPE_PATTERN = /\\u001b\\[[0-9;]*[A-Za-z]/g;\n\nexport interface WorkflowHomeInput {\n /** process.env.WORKFLOW_MCP_HOME, supplied by the caller. */\n envValue: string | undefined;\n /** os.homedir(), supplied by the caller. */\n homeDir: string;\n}\n\n/** Where workflow-mcp keeps its registry, mirroring the engine's own resolution. */\nexport function resolveWorkflowHome(input: WorkflowHomeInput): string {\n return resolve(\n input.envValue !== undefined && input.envValue !== ''\n ? input.envValue\n : resolve(input.homeDir, DEFAULT_WORKFLOW_HOME_DIR_NAME),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(ANSI_ESCAPE_PATTERN, '').trim();\n}\n\n/**\n * One run record plus the facts the hub needs for session scoping but the\n * page never sees.\n */\nexport interface ParsedWorkflowRun {\n /** The wire view, before the progress log is folded in (jobs empty). */\n view: WorkflowRunView;\n /** env.PI_SESSION_ID from the record, when the launcher stamped one. */\n piSessionId?: string;\n /** The repository the run was launched from, when recorded. */\n originalRepoPath?: string;\n}\n\n/**\n * Validates one run.json into the wire shape plus scoping facts.\n *\n * Returns undefined for anything unreadable or a foreign format. The record\n * is trusted for its own stage: the registry rewrites it when a run moves\n * between stage directories.\n */\nexport function parseWorkflowRunRecord(raw: string): ParsedWorkflowRun | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed)) return undefined;\n const runKey = asOptionalString(parsed.runKey);\n const workspace = asOptionalString(parsed.workspace);\n const workflowPath = asOptionalString(parsed.workflowPath);\n const startedAt = asOptionalString(parsed.startedAt);\n const stage = asOptionalString(parsed.stage);\n if (!runKey || !workspace || !workflowPath || !startedAt) return undefined;\n if (stage === undefined || !WORKFLOW_STAGES.includes(stage as WorkflowStage)) return undefined;\n\n const outcome = asOptionalString(parsed.outcome);\n const executionState = asOptionalString(parsed.executionState);\n const rawError = asOptionalString(parsed.errorMessage);\n const env = isRecord(parsed.env) ? parsed.env : undefined;\n const view: WorkflowRunView = {\n runKey,\n workspace,\n displayName: asOptionalString(parsed.displayName) ?? runKey,\n ...(asOptionalString(parsed.workflowName) === undefined\n ? {}\n : { workflowName: asOptionalString(parsed.workflowName) }),\n workflowPath,\n stage: stage as WorkflowStage,\n ...(outcome !== undefined && OUTCOMES.has(outcome) ? { outcome: outcome as WorkflowOutcome } : {}),\n ...(executionState !== undefined && EXECUTION_STATES.has(executionState)\n ? { executionState: executionState as WorkflowRunView['executionState'] }\n : {}),\n ...(asOptionalString(parsed.prompt) === undefined ? {} : { prompt: asOptionalString(parsed.prompt) }),\n startedAt,\n ...(asOptionalString(parsed.finishedAt) === undefined ? {} : { finishedAt: asOptionalString(parsed.finishedAt) }),\n ...(rawError === undefined ? {} : { errorMessage: stripAnsi(rawError) }),\n ...(asOptionalString(parsed.failedJob) === undefined ? {} : { failedJob: asOptionalString(parsed.failedJob) }),\n ...(parsed.stale === true ? { stale: true } : {}),\n ...(asOptionalString(parsed.staleReason) === undefined\n ? {}\n : { staleReason: asOptionalString(parsed.staleReason) }),\n ...(asOptionalString(parsed.worktreeBranch) === undefined\n ? {}\n : { worktreeBranch: asOptionalString(parsed.worktreeBranch) }),\n jobs: [],\n };\n const piSessionId = env === undefined ? undefined : asOptionalString(env[PI_SESSION_ENV]);\n const originalRepoPath = asOptionalString(parsed.originalRepoPath);\n return {\n view,\n ...(piSessionId === undefined ? {} : { piSessionId }),\n ...(originalRepoPath === undefined ? {} : { originalRepoPath }),\n };\n}\n\ninterface WorkflowProgressEvent {\n type: 'job' | 'step';\n status: WorkflowProgressState;\n job: string;\n step?: string;\n index?: number;\n total?: number;\n reason?: string;\n at: string;\n}\n\n/**\n * Parses the append-only progress log. Malformed lines are skipped rather\n * than failing the file: a reader routinely catches a torn final line\n * mid-append.\n */\nexport function parseWorkflowProgress(raw: string): WorkflowProgressEvent[] {\n const events: WorkflowProgressEvent[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!isRecord(parsed)) continue;\n const type = parsed.type;\n const status = asOptionalString(parsed.status);\n const job = asOptionalString(parsed.job);\n const at = asOptionalString(parsed.at);\n if ((type !== 'job' && type !== 'step') || !job || !at) continue;\n if (status === undefined || !PROGRESS_STATES.has(status)) continue;\n const step = asOptionalString(parsed.step);\n const index = asOptionalNumber(parsed.index);\n const total = asOptionalNumber(parsed.total);\n const reason = asOptionalString(parsed.reason);\n events.push({\n type,\n status: status as WorkflowProgressState,\n job,\n ...(step === undefined ? {} : { step }),\n ...(index === undefined ? {} : { index }),\n ...(total === undefined ? {} : { total }),\n ...(reason === undefined ? {} : { reason }),\n at,\n });\n }\n return events;\n}\n\nfunction jobPhase(name: string): WorkflowJobPhase {\n if (name === PHASE_JOB_PRE) return PHASE_JOB_PRE;\n if (name === PHASE_JOB_POST) return PHASE_JOB_POST;\n return 'job';\n}\n\n/**\n * Folds the event log into the current job tree; later events win, so a job\n * that started and later failed reads as failed. Jobs and steps keep the\n * order of their first appearance.\n */\nexport function foldWorkflowProgress(events: readonly WorkflowProgressEvent[]): WorkflowJobView[] {\n const jobs: WorkflowJobView[] = [];\n const jobByName = new Map<string, WorkflowJobView>();\n for (const event of events) {\n let job = jobByName.get(event.job);\n if (job === undefined) {\n job = { name: event.job, phase: jobPhase(event.job), status: event.status, steps: [] };\n jobByName.set(event.job, job);\n jobs.push(job);\n }\n if (event.type === 'job') {\n job.status = event.status;\n if (event.reason !== undefined) job.reason = event.reason;\n if (event.index !== undefined) job.index = event.index;\n if (event.total !== undefined) job.total = event.total;\n if (event.status === 'running' && job.startedAt === undefined) job.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) job.endedAt = event.at;\n continue;\n }\n if (event.step === undefined) continue;\n let step: WorkflowStepView | undefined = job.steps.find((candidate) => candidate.name === event.step);\n if (step === undefined) {\n step = { name: event.step, status: event.status };\n job.steps.push(step);\n }\n step.status = event.status;\n if (event.reason !== undefined) step.reason = event.reason;\n if (event.status === 'running' && step.startedAt === undefined) step.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;\n }\n return jobs;\n}\n\nconst ACTIVE_PROGRESS_STATES: ReadonlySet<WorkflowProgressState> = new Set([\n 'running',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\n\n/** The job and step a run is on right now, for the NOW breadcrumb. */\nexport function workflowPosition(jobs: readonly WorkflowJobView[]): WorkflowPosition | undefined {\n for (let jobIndex = jobs.length - 1; jobIndex >= 0; jobIndex -= 1) {\n const job = jobs[jobIndex]!;\n if (!ACTIVE_PROGRESS_STATES.has(job.status)) continue;\n for (let stepIndex = job.steps.length - 1; stepIndex >= 0; stepIndex -= 1) {\n const step = job.steps[stepIndex]!;\n if (ACTIVE_PROGRESS_STATES.has(step.status)) {\n return {\n job: job.name,\n step: step.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n }\n return {\n job: job.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n return undefined;\n}\n\n/** The finished view: record fields plus the folded job tree and position. */\nexport function completeWorkflowRunView(view: WorkflowRunView, jobs: WorkflowJobView[]): WorkflowRunView {\n const position = view.stage === 'running' ? workflowPosition(jobs) : undefined;\n return { ...view, jobs, ...(position === undefined ? {} : { position }) };\n}\n\nexport interface WorkflowScopeInput {\n sessionId: string;\n /** The session's working directory; repo-local runs belong to it too. */\n cwd: string;\n}\n\nfunction pathsOverlap(left: string, right: string): boolean {\n return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);\n}\n\n/**\n * Whether a run belongs on a session's workflow tab: launched by that Pi\n * session (the same env test doompi-workflow's isSessionRun applies), or\n * launched from the repository the session works in. Containment goes both\n * ways so a session rooted in a subdirectory still sees its repo's runs.\n */\nexport function runBelongsToSession(run: ParsedWorkflowRun, scope: WorkflowScopeInput): boolean {\n if (run.piSessionId !== undefined && run.piSessionId === scope.sessionId) return true;\n if (run.originalRepoPath !== undefined && pathsOverlap(run.originalRepoPath, scope.cwd)) return true;\n return pathsOverlap(run.view.workflowPath, scope.cwd);\n}\n\nfunction parseTime(value: string | undefined): number {\n if (value === undefined) return 0;\n const time = Date.parse(value);\n return Number.isFinite(time) ? time : 0;\n}\n\nconst STAGE_ORDER: Readonly<Record<WorkflowStage, number>> = { running: 0, error: 1, completed: 2 };\n\n/**\n * Whether a run still belongs in the view.\n *\n * Only errored runs age out here, and slowly: everything else a session\n * started stays until the engine forgets the run itself.\n */\nfunction withinRetention(run: WorkflowRunView, now: number): boolean {\n if (run.stage !== 'error') return true;\n const settledAt = parseTime(run.finishedAt) || parseTime(run.startedAt);\n return now - settledAt < WORKFLOW_ERROR_RETENTION_MS;\n}\n\n/**\n * When a run last moved, which is what orders it within its group.\n *\n * A running run has only its start; a settled one is ordered by when it\n * settled, because a list kept for the whole session is read as a history and\n * two runs started together can finish an hour apart.\n */\nfunction lastMovedAt(run: WorkflowRunView): number {\n return run.stage === 'running' ? parseTime(run.startedAt) : parseTime(run.finishedAt) || parseTime(run.startedAt);\n}\n\n/**\n * Running runs first, then errored (recovery acts on them), then finished;\n * newest first within a group, and each group capped on its own.\n */\nexport function presentWorkflowRuns(runs: readonly WorkflowRunView[], now: number): WorkflowRunView[] {\n const groups = new Map<WorkflowStage, WorkflowRunView[]>();\n for (const run of runs) {\n if (!withinRetention(run, now)) continue;\n const group = groups.get(run.stage);\n if (group) group.push(run);\n else groups.set(run.stage, [run]);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right])\n .flatMap(([, group]) =>\n group\n .sort((left, right) => lastMovedAt(right) - lastMovedAt(left))\n .slice(0, MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workflowRuns.cjs","names":["resolve"],"sources":["../../src/services/workflowRuns.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type {\n WorkflowJobPhase,\n WorkflowJobView,\n WorkflowOutcome,\n WorkflowPosition,\n WorkflowProgressState,\n WorkflowRunView,\n WorkflowStage,\n WorkflowStepView,\n} from '../types/webWorkflows.ts';\n\n/**\n * MIRRORS @agimon-ai/workflow-mcp's on-disk registry\n * (src/services/WorkflowRegistryService.ts and WorkflowProgressService.ts in\n * that package). The engine keeps one directory per run under\n * `<home>/workspaces/<workspace>/<stage>/<runKey>/` holding a `run.json`\n * record and an append-only `progress.ndjson` job/step log; the hub only ever\n * reads them. The derivation is duplicated rather than imported because\n * doompi-web must not depend on the engine at runtime; the unit suite pins\n * this mirror against the package's published types.\n */\nconst DEFAULT_WORKFLOW_HOME_DIR_NAME = '.workflow-mcp';\nexport const WORKFLOW_HOME_ENV = 'WORKFLOW_MCP_HOME';\nexport const WORKSPACES_DIR_NAME = 'workspaces';\nexport const RUN_RECORD_FILE_NAME = 'run.json';\nexport const PROGRESS_FILE_NAME = 'progress.ndjson';\nexport const WORKFLOW_STAGES: readonly WorkflowStage[] = ['running', 'completed', 'error'];\n\n/**\n * Errored runs stay a day, which is how long recovery has to act on them.\n *\n * Finished runs have no retention of their own: a session keeps every run it\n * launched, grouped by outcome, for as long as the session lives. A run leaves\n * the view when the engine prunes its registry directory, not on a timer here,\n * because \"what did this session run today\" is a question asked long after the\n * run settled.\n */\nexport const WORKFLOW_ERROR_RETENTION_MS = 24 * 60 * 60 * 1000;\n/**\n * Upper bound per outcome group rather than over the whole list.\n *\n * One cap across every run let a long stream of green runs push a failure out\n * of the list, which is the one thing that must never fall off the end.\n */\nexport const MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP = 24;\n\n/** The env key doompi-workflow stamps on runs it launches; ties a run to a Pi session. */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\nconst PROGRESS_STATES: ReadonlySet<string> = new Set([\n 'running',\n 'completed',\n 'skipped',\n 'failed',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\nconst OUTCOMES: ReadonlySet<string> = new Set(['success', 'skipped', 'failed', 'interrupted']);\nconst EXECUTION_STATES: ReadonlySet<string> = new Set(['running', 'pause_requested', 'paused', 'resume_requested']);\nconst STEP_TERMINAL_STATES: ReadonlySet<WorkflowProgressState> = new Set(['completed', 'skipped', 'failed']);\n/** The engine records its pre:/post: blocks under these reserved pseudo-job names. */\nconst PHASE_JOB_PRE = 'pre';\nconst PHASE_JOB_POST = 'post';\n\n// oxlint-disable-next-line no-control-regex -- stripping terminal color codes from errorMessage is the point\nconst ANSI_ESCAPE_PATTERN = /\\u001b\\[[0-9;]*[A-Za-z]/g;\n\nexport interface WorkflowHomeInput {\n /** process.env.WORKFLOW_MCP_HOME, supplied by the caller. */\n envValue: string | undefined;\n /** os.homedir(), supplied by the caller. */\n homeDir: string;\n}\n\n/** Where workflow-mcp keeps its registry, mirroring the engine's own resolution. */\nexport function resolveWorkflowHome(input: WorkflowHomeInput): string {\n return resolve(\n input.envValue !== undefined && input.envValue !== ''\n ? input.envValue\n : resolve(input.homeDir, DEFAULT_WORKFLOW_HOME_DIR_NAME),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(ANSI_ESCAPE_PATTERN, '').trim();\n}\n\n/**\n * One run record plus the facts the hub needs for session scoping but the\n * page never sees.\n */\nexport interface ParsedWorkflowRun {\n /** The wire view, before the progress log is folded in (jobs empty). */\n view: WorkflowRunView;\n /** env.PI_SESSION_ID from the record, when the launcher stamped one. */\n piSessionId?: string;\n}\n\n/**\n * Validates one run.json into the wire shape plus scoping facts.\n *\n * Returns undefined for anything unreadable or a foreign format. The record\n * is trusted for its own stage: the registry rewrites it when a run moves\n * between stage directories.\n */\nexport function parseWorkflowRunRecord(raw: string): ParsedWorkflowRun | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed)) return undefined;\n const runKey = asOptionalString(parsed.runKey);\n const workspace = asOptionalString(parsed.workspace);\n const workflowPath = asOptionalString(parsed.workflowPath);\n const startedAt = asOptionalString(parsed.startedAt);\n const stage = asOptionalString(parsed.stage);\n if (!runKey || !workspace || !workflowPath || !startedAt) return undefined;\n if (stage === undefined || !WORKFLOW_STAGES.includes(stage as WorkflowStage)) return undefined;\n\n const outcome = asOptionalString(parsed.outcome);\n const executionState = asOptionalString(parsed.executionState);\n const rawError = asOptionalString(parsed.errorMessage);\n const env = isRecord(parsed.env) ? parsed.env : undefined;\n const view: WorkflowRunView = {\n runKey,\n workspace,\n displayName: asOptionalString(parsed.displayName) ?? runKey,\n ...(asOptionalString(parsed.workflowName) === undefined\n ? {}\n : { workflowName: asOptionalString(parsed.workflowName) }),\n workflowPath,\n stage: stage as WorkflowStage,\n ...(outcome !== undefined && OUTCOMES.has(outcome) ? { outcome: outcome as WorkflowOutcome } : {}),\n ...(executionState !== undefined && EXECUTION_STATES.has(executionState)\n ? { executionState: executionState as WorkflowRunView['executionState'] }\n : {}),\n ...(asOptionalString(parsed.prompt) === undefined ? {} : { prompt: asOptionalString(parsed.prompt) }),\n startedAt,\n ...(asOptionalString(parsed.finishedAt) === undefined ? {} : { finishedAt: asOptionalString(parsed.finishedAt) }),\n ...(rawError === undefined ? {} : { errorMessage: stripAnsi(rawError) }),\n ...(asOptionalString(parsed.failedJob) === undefined ? {} : { failedJob: asOptionalString(parsed.failedJob) }),\n ...(parsed.stale === true ? { stale: true } : {}),\n ...(asOptionalString(parsed.staleReason) === undefined\n ? {}\n : { staleReason: asOptionalString(parsed.staleReason) }),\n ...(asOptionalString(parsed.worktreeBranch) === undefined\n ? {}\n : { worktreeBranch: asOptionalString(parsed.worktreeBranch) }),\n jobs: [],\n };\n const piSessionId = env === undefined ? undefined : asOptionalString(env[PI_SESSION_ENV]);\n return { view, ...(piSessionId === undefined ? {} : { piSessionId }) };\n}\n\ninterface WorkflowProgressEvent {\n type: 'job' | 'step';\n status: WorkflowProgressState;\n job: string;\n step?: string;\n index?: number;\n total?: number;\n reason?: string;\n at: string;\n}\n\n/**\n * Parses the append-only progress log. Malformed lines are skipped rather\n * than failing the file: a reader routinely catches a torn final line\n * mid-append.\n */\nexport function parseWorkflowProgress(raw: string): WorkflowProgressEvent[] {\n const events: WorkflowProgressEvent[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!isRecord(parsed)) continue;\n const type = parsed.type;\n const status = asOptionalString(parsed.status);\n const job = asOptionalString(parsed.job);\n const at = asOptionalString(parsed.at);\n if ((type !== 'job' && type !== 'step') || !job || !at) continue;\n if (status === undefined || !PROGRESS_STATES.has(status)) continue;\n const step = asOptionalString(parsed.step);\n const index = asOptionalNumber(parsed.index);\n const total = asOptionalNumber(parsed.total);\n const reason = asOptionalString(parsed.reason);\n events.push({\n type,\n status: status as WorkflowProgressState,\n job,\n ...(step === undefined ? {} : { step }),\n ...(index === undefined ? {} : { index }),\n ...(total === undefined ? {} : { total }),\n ...(reason === undefined ? {} : { reason }),\n at,\n });\n }\n return events;\n}\n\nfunction jobPhase(name: string): WorkflowJobPhase {\n if (name === PHASE_JOB_PRE) return PHASE_JOB_PRE;\n if (name === PHASE_JOB_POST) return PHASE_JOB_POST;\n return 'job';\n}\n\n/**\n * Folds the event log into the current job tree; later events win, so a job\n * that started and later failed reads as failed. Jobs and steps keep the\n * order of their first appearance.\n */\nexport function foldWorkflowProgress(events: readonly WorkflowProgressEvent[]): WorkflowJobView[] {\n const jobs: WorkflowJobView[] = [];\n const jobByName = new Map<string, WorkflowJobView>();\n for (const event of events) {\n let job = jobByName.get(event.job);\n if (job === undefined) {\n job = { name: event.job, phase: jobPhase(event.job), status: event.status, steps: [] };\n jobByName.set(event.job, job);\n jobs.push(job);\n }\n if (event.type === 'job') {\n job.status = event.status;\n if (event.reason !== undefined) job.reason = event.reason;\n if (event.index !== undefined) job.index = event.index;\n if (event.total !== undefined) job.total = event.total;\n if (event.status === 'running' && job.startedAt === undefined) job.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) job.endedAt = event.at;\n continue;\n }\n if (event.step === undefined) continue;\n let step: WorkflowStepView | undefined = job.steps.find((candidate) => candidate.name === event.step);\n if (step === undefined) {\n step = { name: event.step, status: event.status };\n job.steps.push(step);\n }\n step.status = event.status;\n if (event.reason !== undefined) step.reason = event.reason;\n if (event.status === 'running' && step.startedAt === undefined) step.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;\n }\n return jobs;\n}\n\nconst ACTIVE_PROGRESS_STATES: ReadonlySet<WorkflowProgressState> = new Set([\n 'running',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\n\n/** The job and step a run is on right now, for the NOW breadcrumb. */\nexport function workflowPosition(jobs: readonly WorkflowJobView[]): WorkflowPosition | undefined {\n for (let jobIndex = jobs.length - 1; jobIndex >= 0; jobIndex -= 1) {\n const job = jobs[jobIndex]!;\n if (!ACTIVE_PROGRESS_STATES.has(job.status)) continue;\n for (let stepIndex = job.steps.length - 1; stepIndex >= 0; stepIndex -= 1) {\n const step = job.steps[stepIndex]!;\n if (ACTIVE_PROGRESS_STATES.has(step.status)) {\n return {\n job: job.name,\n step: step.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n }\n return {\n job: job.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n return undefined;\n}\n\n/** The finished view: record fields plus the folded job tree and position. */\nexport function completeWorkflowRunView(view: WorkflowRunView, jobs: WorkflowJobView[]): WorkflowRunView {\n const position = view.stage === 'running' ? workflowPosition(jobs) : undefined;\n return { ...view, jobs, ...(position === undefined ? {} : { position }) };\n}\n\n/**\n * Whether a run belongs on a session's workflow tab: launched by that Pi\n * session, which is the same env test doompi-workflow's isSessionRun applies.\n *\n * The registry is one directory under $HOME shared by every repository and\n * every Pi session on the machine, so repository proximity is not ownership.\n * Scoping on it as well let two sessions open in one repo read each other's\n * history, and kept a dead session's runs on the board of every session that\n * replaced it. Fails closed on an unstamped record, which is what a launch\n * from the CLI rather than from a session leaves behind.\n */\nexport function runBelongsToSession(run: ParsedWorkflowRun, sessionId: string): boolean {\n return run.piSessionId !== undefined && run.piSessionId === sessionId;\n}\n\nfunction parseTime(value: string | undefined): number {\n if (value === undefined) return 0;\n const time = Date.parse(value);\n return Number.isFinite(time) ? time : 0;\n}\n\nconst STAGE_ORDER: Readonly<Record<WorkflowStage, number>> = { running: 0, error: 1, completed: 2 };\n\n/**\n * Whether a run still belongs in the view.\n *\n * Only errored runs age out here, and slowly: everything else a session\n * started stays until the engine forgets the run itself.\n */\nfunction withinRetention(run: WorkflowRunView, now: number): boolean {\n if (run.stage !== 'error') return true;\n const settledAt = parseTime(run.finishedAt) || parseTime(run.startedAt);\n return now - settledAt < WORKFLOW_ERROR_RETENTION_MS;\n}\n\n/**\n * When a run last moved, which is what orders it within its group.\n *\n * A running run has only its start; a settled one is ordered by when it\n * settled, because a list kept for the whole session is read as a history and\n * two runs started together can finish an hour apart.\n */\nfunction lastMovedAt(run: WorkflowRunView): number {\n return run.stage === 'running' ? parseTime(run.startedAt) : parseTime(run.finishedAt) || parseTime(run.startedAt);\n}\n\n/**\n * Running runs first, then errored (recovery acts on them), then finished;\n * newest first within a group, and each group capped on its own.\n */\nexport function presentWorkflowRuns(runs: readonly WorkflowRunView[], now: number): WorkflowRunView[] {\n const groups = new Map<WorkflowStage, WorkflowRunView[]>();\n for (const run of runs) {\n if (!withinRetention(run, now)) continue;\n const group = groups.get(run.stage);\n if (group) group.push(run);\n else groups.set(run.stage, [run]);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right])\n .flatMap(([, group]) =>\n group\n .sort((left, right) => lastMovedAt(right) - lastMovedAt(left))\n .slice(0, MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP),\n );\n}\n"],"mappings":"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"}
|
|
@@ -9,8 +9,6 @@ interface ParsedWorkflowRun {
|
|
|
9
9
|
view: WorkflowRunView;
|
|
10
10
|
/** env.PI_SESSION_ID from the record, when the launcher stamped one. */
|
|
11
11
|
piSessionId?: string;
|
|
12
|
-
/** The repository the run was launched from, when recorded. */
|
|
13
|
-
originalRepoPath?: string;
|
|
14
12
|
}
|
|
15
13
|
//#endregion
|
|
16
14
|
export { ParsedWorkflowRun };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowRuns.d.cts","names":[],"sources":["../../src/services/workflowRuns.ts"],"mappings":";;;;;;UAyGiB;;EAEf,MAAM;;EAEN
|
|
1
|
+
{"version":3,"file":"workflowRuns.d.cts","names":[],"sources":["../../src/services/workflowRuns.ts"],"mappings":";;;;;;UAyGiB;;EAEf,MAAM;;EAEN"}
|
|
@@ -9,8 +9,6 @@ interface ParsedWorkflowRun {
|
|
|
9
9
|
view: WorkflowRunView;
|
|
10
10
|
/** env.PI_SESSION_ID from the record, when the launcher stamped one. */
|
|
11
11
|
piSessionId?: string;
|
|
12
|
-
/** The repository the run was launched from, when recorded. */
|
|
13
|
-
originalRepoPath?: string;
|
|
14
12
|
}
|
|
15
13
|
//#endregion
|
|
16
14
|
export { ParsedWorkflowRun };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowRuns.d.mts","names":[],"sources":["../../src/services/workflowRuns.ts"],"mappings":";;;;;;UAyGiB;;EAEf,MAAM;;EAEN
|
|
1
|
+
{"version":3,"file":"workflowRuns.d.mts","names":[],"sources":["../../src/services/workflowRuns.ts"],"mappings":";;;;;;UAyGiB;;EAEf,MAAM;;EAEN"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{resolve as e}from"node:path";const t=`WORKFLOW_MCP_HOME`,n=`workspaces`,r=`run.json`,i=`progress.ndjson`,a=[`running`,`completed`,`error`],o=864e5,s=`PI_SESSION_ID`,c=new Set([`running`,`completed`,`skipped`,`failed`,`pause_requested`,`paused`,`resumed`]),l=new Set([`success`,`skipped`,`failed`,`interrupted`]),u=new Set([`running`,`pause_requested`,`paused`,`resume_requested`]),d=new Set([`completed`,`skipped`,`failed`]),f=`post`,p=/\u001b\[[0-9;]*[A-Za-z]/g;function m(t){return e(t.envValue!==void 0&&t.envValue!==``?t.envValue:e(t.homeDir,`.workflow-mcp`))}function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e){return typeof e==`string`&&e!==``?e:void 0}function _(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function v(e){return e.replace(p,``).trim()}function y(e){let t;try{t=JSON.parse(e)}catch{return}if(!h(t))return;let n=g(t.runKey),r=g(t.workspace),i=g(t.workflowPath),o=g(t.startedAt),c=g(t.stage);if(!n||!r||!i||!o||c===void 0||!a.includes(c))return;let d=g(t.outcome),f=g(t.executionState),p=g(t.errorMessage),m=h(t.env)?t.env:void 0,_={runKey:n,workspace:r,displayName:g(t.displayName)??n,...g(t.workflowName)===void 0?{}:{workflowName:g(t.workflowName)},workflowPath:i,stage:c,...d!==void 0&&l.has(d)?{outcome:d}:{},...f!==void 0&&u.has(f)?{executionState:f}:{},...g(t.prompt)===void 0?{}:{prompt:g(t.prompt)},startedAt:o,...g(t.finishedAt)===void 0?{}:{finishedAt:g(t.finishedAt)},...p===void 0?{}:{errorMessage:v(p)},...g(t.failedJob)===void 0?{}:{failedJob:g(t.failedJob)},...t.stale===!0?{stale:!0}:{},...g(t.staleReason)===void 0?{}:{staleReason:g(t.staleReason)},...g(t.worktreeBranch)===void 0?{}:{worktreeBranch:g(t.worktreeBranch)},jobs:[]},y=m===void 0?void 0:g(m[s])
|
|
2
|
-
`)){let e=n.trim();if(e===``)continue;let r;try{r=JSON.parse(e)}catch{continue}if(!h(r))continue;let i=r.type,a=g(r.status),o=g(r.job),s=g(r.at);if(i!==`job`&&i!==`step`||!o||!s||a===void 0||!c.has(a))continue;let l=g(r.step),u=_(r.index),d=_(r.total),f=g(r.reason);t.push({type:i,status:a,job:o,...l===void 0?{}:{step:l},...u===void 0?{}:{index:u},...d===void 0?{}:{total:d},...f===void 0?{}:{reason:f},at:s})}return t}function x(e){return e===`pre`?`pre`:e===f?f:`job`}function S(e){let t=[],n=new Map;for(let r of e){let e=n.get(r.job);if(e===void 0&&(e={name:r.job,phase:x(r.job),status:r.status,steps:[]},n.set(r.job,e),t.push(e)),r.type===`job`){e.status=r.status,r.reason!==void 0&&(e.reason=r.reason),r.index!==void 0&&(e.index=r.index),r.total!==void 0&&(e.total=r.total),r.status===`running`&&e.startedAt===void 0&&(e.startedAt=r.at),d.has(r.status)&&(e.endedAt=r.at);continue}if(r.step===void 0)continue;let i=e.steps.find(e=>e.name===r.step);i===void 0&&(i={name:r.step,status:r.status},e.steps.push(i)),i.status=r.status,r.reason!==void 0&&(i.reason=r.reason),r.status===`running`&&i.startedAt===void 0&&(i.startedAt=r.at),d.has(r.status)&&(i.endedAt=r.at)}return t}const C=new Set([`running`,`pause_requested`,`paused`,`resumed`]);function w(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(C.has(n.status)){for(let e=n.steps.length-1;e>=0;--e){let t=n.steps[e];if(C.has(t.status))return{job:n.name,step:t.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}return{job:n.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}}}function T(e,t){let n=e.stage===`running`?w(t):void 0;return{...e,jobs:t,...n===void 0?{}:{position:n}}}function E(e,t){return e
|
|
1
|
+
import{resolve as e}from"node:path";const t=`WORKFLOW_MCP_HOME`,n=`workspaces`,r=`run.json`,i=`progress.ndjson`,a=[`running`,`completed`,`error`],o=864e5,s=`PI_SESSION_ID`,c=new Set([`running`,`completed`,`skipped`,`failed`,`pause_requested`,`paused`,`resumed`]),l=new Set([`success`,`skipped`,`failed`,`interrupted`]),u=new Set([`running`,`pause_requested`,`paused`,`resume_requested`]),d=new Set([`completed`,`skipped`,`failed`]),f=`post`,p=/\u001b\[[0-9;]*[A-Za-z]/g;function m(t){return e(t.envValue!==void 0&&t.envValue!==``?t.envValue:e(t.homeDir,`.workflow-mcp`))}function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e){return typeof e==`string`&&e!==``?e:void 0}function _(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function v(e){return e.replace(p,``).trim()}function y(e){let t;try{t=JSON.parse(e)}catch{return}if(!h(t))return;let n=g(t.runKey),r=g(t.workspace),i=g(t.workflowPath),o=g(t.startedAt),c=g(t.stage);if(!n||!r||!i||!o||c===void 0||!a.includes(c))return;let d=g(t.outcome),f=g(t.executionState),p=g(t.errorMessage),m=h(t.env)?t.env:void 0,_={runKey:n,workspace:r,displayName:g(t.displayName)??n,...g(t.workflowName)===void 0?{}:{workflowName:g(t.workflowName)},workflowPath:i,stage:c,...d!==void 0&&l.has(d)?{outcome:d}:{},...f!==void 0&&u.has(f)?{executionState:f}:{},...g(t.prompt)===void 0?{}:{prompt:g(t.prompt)},startedAt:o,...g(t.finishedAt)===void 0?{}:{finishedAt:g(t.finishedAt)},...p===void 0?{}:{errorMessage:v(p)},...g(t.failedJob)===void 0?{}:{failedJob:g(t.failedJob)},...t.stale===!0?{stale:!0}:{},...g(t.staleReason)===void 0?{}:{staleReason:g(t.staleReason)},...g(t.worktreeBranch)===void 0?{}:{worktreeBranch:g(t.worktreeBranch)},jobs:[]},y=m===void 0?void 0:g(m[s]);return{view:_,...y===void 0?{}:{piSessionId:y}}}function b(e){let t=[];for(let n of e.split(`
|
|
2
|
+
`)){let e=n.trim();if(e===``)continue;let r;try{r=JSON.parse(e)}catch{continue}if(!h(r))continue;let i=r.type,a=g(r.status),o=g(r.job),s=g(r.at);if(i!==`job`&&i!==`step`||!o||!s||a===void 0||!c.has(a))continue;let l=g(r.step),u=_(r.index),d=_(r.total),f=g(r.reason);t.push({type:i,status:a,job:o,...l===void 0?{}:{step:l},...u===void 0?{}:{index:u},...d===void 0?{}:{total:d},...f===void 0?{}:{reason:f},at:s})}return t}function x(e){return e===`pre`?`pre`:e===f?f:`job`}function S(e){let t=[],n=new Map;for(let r of e){let e=n.get(r.job);if(e===void 0&&(e={name:r.job,phase:x(r.job),status:r.status,steps:[]},n.set(r.job,e),t.push(e)),r.type===`job`){e.status=r.status,r.reason!==void 0&&(e.reason=r.reason),r.index!==void 0&&(e.index=r.index),r.total!==void 0&&(e.total=r.total),r.status===`running`&&e.startedAt===void 0&&(e.startedAt=r.at),d.has(r.status)&&(e.endedAt=r.at);continue}if(r.step===void 0)continue;let i=e.steps.find(e=>e.name===r.step);i===void 0&&(i={name:r.step,status:r.status},e.steps.push(i)),i.status=r.status,r.reason!==void 0&&(i.reason=r.reason),r.status===`running`&&i.startedAt===void 0&&(i.startedAt=r.at),d.has(r.status)&&(i.endedAt=r.at)}return t}const C=new Set([`running`,`pause_requested`,`paused`,`resumed`]);function w(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(C.has(n.status)){for(let e=n.steps.length-1;e>=0;--e){let t=n.steps[e];if(C.has(t.status))return{job:n.name,step:t.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}return{job:n.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}}}function T(e,t){let n=e.stage===`running`?w(t):void 0;return{...e,jobs:t,...n===void 0?{}:{position:n}}}function E(e,t){return e.piSessionId!==void 0&&e.piSessionId===t}function D(e){if(e===void 0)return 0;let t=Date.parse(e);return Number.isFinite(t)?t:0}const O={running:0,error:1,completed:2};function k(e,t){return e.stage!==`error`||t-(D(e.finishedAt)||D(e.startedAt))<o}function A(e){return e.stage===`running`?D(e.startedAt):D(e.finishedAt)||D(e.startedAt)}function j(e,t){let n=new Map;for(let r of e){if(!k(r,t))continue;let e=n.get(r.stage);e?e.push(r):n.set(r.stage,[r])}return[...n.entries()].sort(([e],[t])=>O[e]-O[t]).flatMap(([,e])=>e.sort((e,t)=>A(t)-A(e)).slice(0,24))}export{s as PI_SESSION_ENV,i as PROGRESS_FILE_NAME,r as RUN_RECORD_FILE_NAME,o as WORKFLOW_ERROR_RETENTION_MS,t as WORKFLOW_HOME_ENV,a as WORKFLOW_STAGES,n as WORKSPACES_DIR_NAME,T as completeWorkflowRunView,S as foldWorkflowProgress,b as parseWorkflowProgress,y as parseWorkflowRunRecord,j as presentWorkflowRuns,m as resolveWorkflowHome,E as runBelongsToSession,w as workflowPosition};
|
|
3
3
|
//# sourceMappingURL=workflowRuns.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflowRuns.mjs","names":[],"sources":["../../src/services/workflowRuns.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type {\n WorkflowJobPhase,\n WorkflowJobView,\n WorkflowOutcome,\n WorkflowPosition,\n WorkflowProgressState,\n WorkflowRunView,\n WorkflowStage,\n WorkflowStepView,\n} from '../types/webWorkflows.ts';\n\n/**\n * MIRRORS @agimon-ai/workflow-mcp's on-disk registry\n * (src/services/WorkflowRegistryService.ts and WorkflowProgressService.ts in\n * that package). The engine keeps one directory per run under\n * `<home>/workspaces/<workspace>/<stage>/<runKey>/` holding a `run.json`\n * record and an append-only `progress.ndjson` job/step log; the hub only ever\n * reads them. The derivation is duplicated rather than imported because\n * doompi-web must not depend on the engine at runtime; the unit suite pins\n * this mirror against the package's published types.\n */\nconst DEFAULT_WORKFLOW_HOME_DIR_NAME = '.workflow-mcp';\nexport const WORKFLOW_HOME_ENV = 'WORKFLOW_MCP_HOME';\nexport const WORKSPACES_DIR_NAME = 'workspaces';\nexport const RUN_RECORD_FILE_NAME = 'run.json';\nexport const PROGRESS_FILE_NAME = 'progress.ndjson';\nexport const WORKFLOW_STAGES: readonly WorkflowStage[] = ['running', 'completed', 'error'];\n\n/**\n * Errored runs stay a day, which is how long recovery has to act on them.\n *\n * Finished runs have no retention of their own: a session keeps every run it\n * started, grouped by outcome, for as long as the session lives. A run leaves\n * the view when the engine prunes its registry directory, not on a timer here,\n * because \"what did this session run today\" is a question asked long after the\n * run settled.\n */\nexport const WORKFLOW_ERROR_RETENTION_MS = 24 * 60 * 60 * 1000;\n/**\n * Upper bound per outcome group rather than over the whole list.\n *\n * One cap across every run let a long stream of green runs push a failure out\n * of the list, which is the one thing that must never fall off the end.\n */\nexport const MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP = 24;\n\n/** The env key doompi-workflow stamps on runs it launches; ties a run to a Pi session. */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\nconst PROGRESS_STATES: ReadonlySet<string> = new Set([\n 'running',\n 'completed',\n 'skipped',\n 'failed',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\nconst OUTCOMES: ReadonlySet<string> = new Set(['success', 'skipped', 'failed', 'interrupted']);\nconst EXECUTION_STATES: ReadonlySet<string> = new Set(['running', 'pause_requested', 'paused', 'resume_requested']);\nconst STEP_TERMINAL_STATES: ReadonlySet<WorkflowProgressState> = new Set(['completed', 'skipped', 'failed']);\n/** The engine records its pre:/post: blocks under these reserved pseudo-job names. */\nconst PHASE_JOB_PRE = 'pre';\nconst PHASE_JOB_POST = 'post';\n\n// oxlint-disable-next-line no-control-regex -- stripping terminal color codes from errorMessage is the point\nconst ANSI_ESCAPE_PATTERN = /\\u001b\\[[0-9;]*[A-Za-z]/g;\n\nexport interface WorkflowHomeInput {\n /** process.env.WORKFLOW_MCP_HOME, supplied by the caller. */\n envValue: string | undefined;\n /** os.homedir(), supplied by the caller. */\n homeDir: string;\n}\n\n/** Where workflow-mcp keeps its registry, mirroring the engine's own resolution. */\nexport function resolveWorkflowHome(input: WorkflowHomeInput): string {\n return resolve(\n input.envValue !== undefined && input.envValue !== ''\n ? input.envValue\n : resolve(input.homeDir, DEFAULT_WORKFLOW_HOME_DIR_NAME),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(ANSI_ESCAPE_PATTERN, '').trim();\n}\n\n/**\n * One run record plus the facts the hub needs for session scoping but the\n * page never sees.\n */\nexport interface ParsedWorkflowRun {\n /** The wire view, before the progress log is folded in (jobs empty). */\n view: WorkflowRunView;\n /** env.PI_SESSION_ID from the record, when the launcher stamped one. */\n piSessionId?: string;\n /** The repository the run was launched from, when recorded. */\n originalRepoPath?: string;\n}\n\n/**\n * Validates one run.json into the wire shape plus scoping facts.\n *\n * Returns undefined for anything unreadable or a foreign format. The record\n * is trusted for its own stage: the registry rewrites it when a run moves\n * between stage directories.\n */\nexport function parseWorkflowRunRecord(raw: string): ParsedWorkflowRun | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed)) return undefined;\n const runKey = asOptionalString(parsed.runKey);\n const workspace = asOptionalString(parsed.workspace);\n const workflowPath = asOptionalString(parsed.workflowPath);\n const startedAt = asOptionalString(parsed.startedAt);\n const stage = asOptionalString(parsed.stage);\n if (!runKey || !workspace || !workflowPath || !startedAt) return undefined;\n if (stage === undefined || !WORKFLOW_STAGES.includes(stage as WorkflowStage)) return undefined;\n\n const outcome = asOptionalString(parsed.outcome);\n const executionState = asOptionalString(parsed.executionState);\n const rawError = asOptionalString(parsed.errorMessage);\n const env = isRecord(parsed.env) ? parsed.env : undefined;\n const view: WorkflowRunView = {\n runKey,\n workspace,\n displayName: asOptionalString(parsed.displayName) ?? runKey,\n ...(asOptionalString(parsed.workflowName) === undefined\n ? {}\n : { workflowName: asOptionalString(parsed.workflowName) }),\n workflowPath,\n stage: stage as WorkflowStage,\n ...(outcome !== undefined && OUTCOMES.has(outcome) ? { outcome: outcome as WorkflowOutcome } : {}),\n ...(executionState !== undefined && EXECUTION_STATES.has(executionState)\n ? { executionState: executionState as WorkflowRunView['executionState'] }\n : {}),\n ...(asOptionalString(parsed.prompt) === undefined ? {} : { prompt: asOptionalString(parsed.prompt) }),\n startedAt,\n ...(asOptionalString(parsed.finishedAt) === undefined ? {} : { finishedAt: asOptionalString(parsed.finishedAt) }),\n ...(rawError === undefined ? {} : { errorMessage: stripAnsi(rawError) }),\n ...(asOptionalString(parsed.failedJob) === undefined ? {} : { failedJob: asOptionalString(parsed.failedJob) }),\n ...(parsed.stale === true ? { stale: true } : {}),\n ...(asOptionalString(parsed.staleReason) === undefined\n ? {}\n : { staleReason: asOptionalString(parsed.staleReason) }),\n ...(asOptionalString(parsed.worktreeBranch) === undefined\n ? {}\n : { worktreeBranch: asOptionalString(parsed.worktreeBranch) }),\n jobs: [],\n };\n const piSessionId = env === undefined ? undefined : asOptionalString(env[PI_SESSION_ENV]);\n const originalRepoPath = asOptionalString(parsed.originalRepoPath);\n return {\n view,\n ...(piSessionId === undefined ? {} : { piSessionId }),\n ...(originalRepoPath === undefined ? {} : { originalRepoPath }),\n };\n}\n\ninterface WorkflowProgressEvent {\n type: 'job' | 'step';\n status: WorkflowProgressState;\n job: string;\n step?: string;\n index?: number;\n total?: number;\n reason?: string;\n at: string;\n}\n\n/**\n * Parses the append-only progress log. Malformed lines are skipped rather\n * than failing the file: a reader routinely catches a torn final line\n * mid-append.\n */\nexport function parseWorkflowProgress(raw: string): WorkflowProgressEvent[] {\n const events: WorkflowProgressEvent[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!isRecord(parsed)) continue;\n const type = parsed.type;\n const status = asOptionalString(parsed.status);\n const job = asOptionalString(parsed.job);\n const at = asOptionalString(parsed.at);\n if ((type !== 'job' && type !== 'step') || !job || !at) continue;\n if (status === undefined || !PROGRESS_STATES.has(status)) continue;\n const step = asOptionalString(parsed.step);\n const index = asOptionalNumber(parsed.index);\n const total = asOptionalNumber(parsed.total);\n const reason = asOptionalString(parsed.reason);\n events.push({\n type,\n status: status as WorkflowProgressState,\n job,\n ...(step === undefined ? {} : { step }),\n ...(index === undefined ? {} : { index }),\n ...(total === undefined ? {} : { total }),\n ...(reason === undefined ? {} : { reason }),\n at,\n });\n }\n return events;\n}\n\nfunction jobPhase(name: string): WorkflowJobPhase {\n if (name === PHASE_JOB_PRE) return PHASE_JOB_PRE;\n if (name === PHASE_JOB_POST) return PHASE_JOB_POST;\n return 'job';\n}\n\n/**\n * Folds the event log into the current job tree; later events win, so a job\n * that started and later failed reads as failed. Jobs and steps keep the\n * order of their first appearance.\n */\nexport function foldWorkflowProgress(events: readonly WorkflowProgressEvent[]): WorkflowJobView[] {\n const jobs: WorkflowJobView[] = [];\n const jobByName = new Map<string, WorkflowJobView>();\n for (const event of events) {\n let job = jobByName.get(event.job);\n if (job === undefined) {\n job = { name: event.job, phase: jobPhase(event.job), status: event.status, steps: [] };\n jobByName.set(event.job, job);\n jobs.push(job);\n }\n if (event.type === 'job') {\n job.status = event.status;\n if (event.reason !== undefined) job.reason = event.reason;\n if (event.index !== undefined) job.index = event.index;\n if (event.total !== undefined) job.total = event.total;\n if (event.status === 'running' && job.startedAt === undefined) job.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) job.endedAt = event.at;\n continue;\n }\n if (event.step === undefined) continue;\n let step: WorkflowStepView | undefined = job.steps.find((candidate) => candidate.name === event.step);\n if (step === undefined) {\n step = { name: event.step, status: event.status };\n job.steps.push(step);\n }\n step.status = event.status;\n if (event.reason !== undefined) step.reason = event.reason;\n if (event.status === 'running' && step.startedAt === undefined) step.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;\n }\n return jobs;\n}\n\nconst ACTIVE_PROGRESS_STATES: ReadonlySet<WorkflowProgressState> = new Set([\n 'running',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\n\n/** The job and step a run is on right now, for the NOW breadcrumb. */\nexport function workflowPosition(jobs: readonly WorkflowJobView[]): WorkflowPosition | undefined {\n for (let jobIndex = jobs.length - 1; jobIndex >= 0; jobIndex -= 1) {\n const job = jobs[jobIndex]!;\n if (!ACTIVE_PROGRESS_STATES.has(job.status)) continue;\n for (let stepIndex = job.steps.length - 1; stepIndex >= 0; stepIndex -= 1) {\n const step = job.steps[stepIndex]!;\n if (ACTIVE_PROGRESS_STATES.has(step.status)) {\n return {\n job: job.name,\n step: step.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n }\n return {\n job: job.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n return undefined;\n}\n\n/** The finished view: record fields plus the folded job tree and position. */\nexport function completeWorkflowRunView(view: WorkflowRunView, jobs: WorkflowJobView[]): WorkflowRunView {\n const position = view.stage === 'running' ? workflowPosition(jobs) : undefined;\n return { ...view, jobs, ...(position === undefined ? {} : { position }) };\n}\n\nexport interface WorkflowScopeInput {\n sessionId: string;\n /** The session's working directory; repo-local runs belong to it too. */\n cwd: string;\n}\n\nfunction pathsOverlap(left: string, right: string): boolean {\n return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);\n}\n\n/**\n * Whether a run belongs on a session's workflow tab: launched by that Pi\n * session (the same env test doompi-workflow's isSessionRun applies), or\n * launched from the repository the session works in. Containment goes both\n * ways so a session rooted in a subdirectory still sees its repo's runs.\n */\nexport function runBelongsToSession(run: ParsedWorkflowRun, scope: WorkflowScopeInput): boolean {\n if (run.piSessionId !== undefined && run.piSessionId === scope.sessionId) return true;\n if (run.originalRepoPath !== undefined && pathsOverlap(run.originalRepoPath, scope.cwd)) return true;\n return pathsOverlap(run.view.workflowPath, scope.cwd);\n}\n\nfunction parseTime(value: string | undefined): number {\n if (value === undefined) return 0;\n const time = Date.parse(value);\n return Number.isFinite(time) ? time : 0;\n}\n\nconst STAGE_ORDER: Readonly<Record<WorkflowStage, number>> = { running: 0, error: 1, completed: 2 };\n\n/**\n * Whether a run still belongs in the view.\n *\n * Only errored runs age out here, and slowly: everything else a session\n * started stays until the engine forgets the run itself.\n */\nfunction withinRetention(run: WorkflowRunView, now: number): boolean {\n if (run.stage !== 'error') return true;\n const settledAt = parseTime(run.finishedAt) || parseTime(run.startedAt);\n return now - settledAt < WORKFLOW_ERROR_RETENTION_MS;\n}\n\n/**\n * When a run last moved, which is what orders it within its group.\n *\n * A running run has only its start; a settled one is ordered by when it\n * settled, because a list kept for the whole session is read as a history and\n * two runs started together can finish an hour apart.\n */\nfunction lastMovedAt(run: WorkflowRunView): number {\n return run.stage === 'running' ? parseTime(run.startedAt) : parseTime(run.finishedAt) || parseTime(run.startedAt);\n}\n\n/**\n * Running runs first, then errored (recovery acts on them), then finished;\n * newest first within a group, and each group capped on its own.\n */\nexport function presentWorkflowRuns(runs: readonly WorkflowRunView[], now: number): WorkflowRunView[] {\n const groups = new Map<WorkflowStage, WorkflowRunView[]>();\n for (const run of runs) {\n if (!withinRetention(run, now)) continue;\n const group = groups.get(run.stage);\n if (group) group.push(run);\n else groups.set(run.stage, [run]);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right])\n .flatMap(([, group]) =>\n group\n .sort((left, right) => lastMovedAt(right) - lastMovedAt(left))\n .slice(0, MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"workflowRuns.mjs","names":[],"sources":["../../src/services/workflowRuns.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type {\n WorkflowJobPhase,\n WorkflowJobView,\n WorkflowOutcome,\n WorkflowPosition,\n WorkflowProgressState,\n WorkflowRunView,\n WorkflowStage,\n WorkflowStepView,\n} from '../types/webWorkflows.ts';\n\n/**\n * MIRRORS @agimon-ai/workflow-mcp's on-disk registry\n * (src/services/WorkflowRegistryService.ts and WorkflowProgressService.ts in\n * that package). The engine keeps one directory per run under\n * `<home>/workspaces/<workspace>/<stage>/<runKey>/` holding a `run.json`\n * record and an append-only `progress.ndjson` job/step log; the hub only ever\n * reads them. The derivation is duplicated rather than imported because\n * doompi-web must not depend on the engine at runtime; the unit suite pins\n * this mirror against the package's published types.\n */\nconst DEFAULT_WORKFLOW_HOME_DIR_NAME = '.workflow-mcp';\nexport const WORKFLOW_HOME_ENV = 'WORKFLOW_MCP_HOME';\nexport const WORKSPACES_DIR_NAME = 'workspaces';\nexport const RUN_RECORD_FILE_NAME = 'run.json';\nexport const PROGRESS_FILE_NAME = 'progress.ndjson';\nexport const WORKFLOW_STAGES: readonly WorkflowStage[] = ['running', 'completed', 'error'];\n\n/**\n * Errored runs stay a day, which is how long recovery has to act on them.\n *\n * Finished runs have no retention of their own: a session keeps every run it\n * launched, grouped by outcome, for as long as the session lives. A run leaves\n * the view when the engine prunes its registry directory, not on a timer here,\n * because \"what did this session run today\" is a question asked long after the\n * run settled.\n */\nexport const WORKFLOW_ERROR_RETENTION_MS = 24 * 60 * 60 * 1000;\n/**\n * Upper bound per outcome group rather than over the whole list.\n *\n * One cap across every run let a long stream of green runs push a failure out\n * of the list, which is the one thing that must never fall off the end.\n */\nexport const MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP = 24;\n\n/** The env key doompi-workflow stamps on runs it launches; ties a run to a Pi session. */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\nconst PROGRESS_STATES: ReadonlySet<string> = new Set([\n 'running',\n 'completed',\n 'skipped',\n 'failed',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\nconst OUTCOMES: ReadonlySet<string> = new Set(['success', 'skipped', 'failed', 'interrupted']);\nconst EXECUTION_STATES: ReadonlySet<string> = new Set(['running', 'pause_requested', 'paused', 'resume_requested']);\nconst STEP_TERMINAL_STATES: ReadonlySet<WorkflowProgressState> = new Set(['completed', 'skipped', 'failed']);\n/** The engine records its pre:/post: blocks under these reserved pseudo-job names. */\nconst PHASE_JOB_PRE = 'pre';\nconst PHASE_JOB_POST = 'post';\n\n// oxlint-disable-next-line no-control-regex -- stripping terminal color codes from errorMessage is the point\nconst ANSI_ESCAPE_PATTERN = /\\u001b\\[[0-9;]*[A-Za-z]/g;\n\nexport interface WorkflowHomeInput {\n /** process.env.WORKFLOW_MCP_HOME, supplied by the caller. */\n envValue: string | undefined;\n /** os.homedir(), supplied by the caller. */\n homeDir: string;\n}\n\n/** Where workflow-mcp keeps its registry, mirroring the engine's own resolution. */\nexport function resolveWorkflowHome(input: WorkflowHomeInput): string {\n return resolve(\n input.envValue !== undefined && input.envValue !== ''\n ? input.envValue\n : resolve(input.homeDir, DEFAULT_WORKFLOW_HOME_DIR_NAME),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(ANSI_ESCAPE_PATTERN, '').trim();\n}\n\n/**\n * One run record plus the facts the hub needs for session scoping but the\n * page never sees.\n */\nexport interface ParsedWorkflowRun {\n /** The wire view, before the progress log is folded in (jobs empty). */\n view: WorkflowRunView;\n /** env.PI_SESSION_ID from the record, when the launcher stamped one. */\n piSessionId?: string;\n}\n\n/**\n * Validates one run.json into the wire shape plus scoping facts.\n *\n * Returns undefined for anything unreadable or a foreign format. The record\n * is trusted for its own stage: the registry rewrites it when a run moves\n * between stage directories.\n */\nexport function parseWorkflowRunRecord(raw: string): ParsedWorkflowRun | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed)) return undefined;\n const runKey = asOptionalString(parsed.runKey);\n const workspace = asOptionalString(parsed.workspace);\n const workflowPath = asOptionalString(parsed.workflowPath);\n const startedAt = asOptionalString(parsed.startedAt);\n const stage = asOptionalString(parsed.stage);\n if (!runKey || !workspace || !workflowPath || !startedAt) return undefined;\n if (stage === undefined || !WORKFLOW_STAGES.includes(stage as WorkflowStage)) return undefined;\n\n const outcome = asOptionalString(parsed.outcome);\n const executionState = asOptionalString(parsed.executionState);\n const rawError = asOptionalString(parsed.errorMessage);\n const env = isRecord(parsed.env) ? parsed.env : undefined;\n const view: WorkflowRunView = {\n runKey,\n workspace,\n displayName: asOptionalString(parsed.displayName) ?? runKey,\n ...(asOptionalString(parsed.workflowName) === undefined\n ? {}\n : { workflowName: asOptionalString(parsed.workflowName) }),\n workflowPath,\n stage: stage as WorkflowStage,\n ...(outcome !== undefined && OUTCOMES.has(outcome) ? { outcome: outcome as WorkflowOutcome } : {}),\n ...(executionState !== undefined && EXECUTION_STATES.has(executionState)\n ? { executionState: executionState as WorkflowRunView['executionState'] }\n : {}),\n ...(asOptionalString(parsed.prompt) === undefined ? {} : { prompt: asOptionalString(parsed.prompt) }),\n startedAt,\n ...(asOptionalString(parsed.finishedAt) === undefined ? {} : { finishedAt: asOptionalString(parsed.finishedAt) }),\n ...(rawError === undefined ? {} : { errorMessage: stripAnsi(rawError) }),\n ...(asOptionalString(parsed.failedJob) === undefined ? {} : { failedJob: asOptionalString(parsed.failedJob) }),\n ...(parsed.stale === true ? { stale: true } : {}),\n ...(asOptionalString(parsed.staleReason) === undefined\n ? {}\n : { staleReason: asOptionalString(parsed.staleReason) }),\n ...(asOptionalString(parsed.worktreeBranch) === undefined\n ? {}\n : { worktreeBranch: asOptionalString(parsed.worktreeBranch) }),\n jobs: [],\n };\n const piSessionId = env === undefined ? undefined : asOptionalString(env[PI_SESSION_ENV]);\n return { view, ...(piSessionId === undefined ? {} : { piSessionId }) };\n}\n\ninterface WorkflowProgressEvent {\n type: 'job' | 'step';\n status: WorkflowProgressState;\n job: string;\n step?: string;\n index?: number;\n total?: number;\n reason?: string;\n at: string;\n}\n\n/**\n * Parses the append-only progress log. Malformed lines are skipped rather\n * than failing the file: a reader routinely catches a torn final line\n * mid-append.\n */\nexport function parseWorkflowProgress(raw: string): WorkflowProgressEvent[] {\n const events: WorkflowProgressEvent[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!isRecord(parsed)) continue;\n const type = parsed.type;\n const status = asOptionalString(parsed.status);\n const job = asOptionalString(parsed.job);\n const at = asOptionalString(parsed.at);\n if ((type !== 'job' && type !== 'step') || !job || !at) continue;\n if (status === undefined || !PROGRESS_STATES.has(status)) continue;\n const step = asOptionalString(parsed.step);\n const index = asOptionalNumber(parsed.index);\n const total = asOptionalNumber(parsed.total);\n const reason = asOptionalString(parsed.reason);\n events.push({\n type,\n status: status as WorkflowProgressState,\n job,\n ...(step === undefined ? {} : { step }),\n ...(index === undefined ? {} : { index }),\n ...(total === undefined ? {} : { total }),\n ...(reason === undefined ? {} : { reason }),\n at,\n });\n }\n return events;\n}\n\nfunction jobPhase(name: string): WorkflowJobPhase {\n if (name === PHASE_JOB_PRE) return PHASE_JOB_PRE;\n if (name === PHASE_JOB_POST) return PHASE_JOB_POST;\n return 'job';\n}\n\n/**\n * Folds the event log into the current job tree; later events win, so a job\n * that started and later failed reads as failed. Jobs and steps keep the\n * order of their first appearance.\n */\nexport function foldWorkflowProgress(events: readonly WorkflowProgressEvent[]): WorkflowJobView[] {\n const jobs: WorkflowJobView[] = [];\n const jobByName = new Map<string, WorkflowJobView>();\n for (const event of events) {\n let job = jobByName.get(event.job);\n if (job === undefined) {\n job = { name: event.job, phase: jobPhase(event.job), status: event.status, steps: [] };\n jobByName.set(event.job, job);\n jobs.push(job);\n }\n if (event.type === 'job') {\n job.status = event.status;\n if (event.reason !== undefined) job.reason = event.reason;\n if (event.index !== undefined) job.index = event.index;\n if (event.total !== undefined) job.total = event.total;\n if (event.status === 'running' && job.startedAt === undefined) job.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) job.endedAt = event.at;\n continue;\n }\n if (event.step === undefined) continue;\n let step: WorkflowStepView | undefined = job.steps.find((candidate) => candidate.name === event.step);\n if (step === undefined) {\n step = { name: event.step, status: event.status };\n job.steps.push(step);\n }\n step.status = event.status;\n if (event.reason !== undefined) step.reason = event.reason;\n if (event.status === 'running' && step.startedAt === undefined) step.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;\n }\n return jobs;\n}\n\nconst ACTIVE_PROGRESS_STATES: ReadonlySet<WorkflowProgressState> = new Set([\n 'running',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\n\n/** The job and step a run is on right now, for the NOW breadcrumb. */\nexport function workflowPosition(jobs: readonly WorkflowJobView[]): WorkflowPosition | undefined {\n for (let jobIndex = jobs.length - 1; jobIndex >= 0; jobIndex -= 1) {\n const job = jobs[jobIndex]!;\n if (!ACTIVE_PROGRESS_STATES.has(job.status)) continue;\n for (let stepIndex = job.steps.length - 1; stepIndex >= 0; stepIndex -= 1) {\n const step = job.steps[stepIndex]!;\n if (ACTIVE_PROGRESS_STATES.has(step.status)) {\n return {\n job: job.name,\n step: step.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n }\n return {\n job: job.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n return undefined;\n}\n\n/** The finished view: record fields plus the folded job tree and position. */\nexport function completeWorkflowRunView(view: WorkflowRunView, jobs: WorkflowJobView[]): WorkflowRunView {\n const position = view.stage === 'running' ? workflowPosition(jobs) : undefined;\n return { ...view, jobs, ...(position === undefined ? {} : { position }) };\n}\n\n/**\n * Whether a run belongs on a session's workflow tab: launched by that Pi\n * session, which is the same env test doompi-workflow's isSessionRun applies.\n *\n * The registry is one directory under $HOME shared by every repository and\n * every Pi session on the machine, so repository proximity is not ownership.\n * Scoping on it as well let two sessions open in one repo read each other's\n * history, and kept a dead session's runs on the board of every session that\n * replaced it. Fails closed on an unstamped record, which is what a launch\n * from the CLI rather than from a session leaves behind.\n */\nexport function runBelongsToSession(run: ParsedWorkflowRun, sessionId: string): boolean {\n return run.piSessionId !== undefined && run.piSessionId === sessionId;\n}\n\nfunction parseTime(value: string | undefined): number {\n if (value === undefined) return 0;\n const time = Date.parse(value);\n return Number.isFinite(time) ? time : 0;\n}\n\nconst STAGE_ORDER: Readonly<Record<WorkflowStage, number>> = { running: 0, error: 1, completed: 2 };\n\n/**\n * Whether a run still belongs in the view.\n *\n * Only errored runs age out here, and slowly: everything else a session\n * started stays until the engine forgets the run itself.\n */\nfunction withinRetention(run: WorkflowRunView, now: number): boolean {\n if (run.stage !== 'error') return true;\n const settledAt = parseTime(run.finishedAt) || parseTime(run.startedAt);\n return now - settledAt < WORKFLOW_ERROR_RETENTION_MS;\n}\n\n/**\n * When a run last moved, which is what orders it within its group.\n *\n * A running run has only its start; a settled one is ordered by when it\n * settled, because a list kept for the whole session is read as a history and\n * two runs started together can finish an hour apart.\n */\nfunction lastMovedAt(run: WorkflowRunView): number {\n return run.stage === 'running' ? parseTime(run.startedAt) : parseTime(run.finishedAt) || parseTime(run.startedAt);\n}\n\n/**\n * Running runs first, then errored (recovery acts on them), then finished;\n * newest first within a group, and each group capped on its own.\n */\nexport function presentWorkflowRuns(runs: readonly WorkflowRunView[], now: number): WorkflowRunView[] {\n const groups = new Map<WorkflowStage, WorkflowRunView[]>();\n for (const run of runs) {\n if (!withinRetention(run, now)) continue;\n const group = groups.get(run.stage);\n if (group) group.push(run);\n else groups.set(run.stage, [run]);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right])\n .flatMap(([, group]) =>\n group\n .sort((left, right) => lastMovedAt(right) - lastMovedAt(left))\n .slice(0, MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP),\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-workflow",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.40",
|
|
4
4
|
"description": "GitHub Actions-style workflow graphs, artifacts, recovery, and asynchronous runs for DoomPi.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -67,11 +67,12 @@
|
|
|
67
67
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
68
68
|
"hono": "4.13.3",
|
|
69
69
|
"zod": "4.4.3",
|
|
70
|
-
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.
|
|
71
|
-
"@agimon-ai/doompi-
|
|
72
|
-
"@agimon-ai/doompi-
|
|
73
|
-
"@agimon-ai/doompi-web-components": "0.0.1-alpha.
|
|
74
|
-
"@agimon-ai/doompi-web-contracts": "0.0.1-alpha.
|
|
70
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.40",
|
|
71
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.40",
|
|
72
|
+
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.40",
|
|
73
|
+
"@agimon-ai/doompi-web-components": "0.0.1-alpha.1",
|
|
74
|
+
"@agimon-ai/doompi-web-contracts": "0.0.1-alpha.3",
|
|
75
|
+
"@agimon-ai/doompi-web-security": "0.0.1-alpha.2"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
77
78
|
"@earendil-works/pi-coding-agent": "0.84.3",
|
|
@@ -54,8 +54,11 @@ function InputField({
|
|
|
54
54
|
.filter((part): part is string => part !== undefined && part !== '')
|
|
55
55
|
.join(' · ');
|
|
56
56
|
return (
|
|
57
|
-
<div
|
|
58
|
-
|
|
57
|
+
<div
|
|
58
|
+
className="flex flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-2.5"
|
|
59
|
+
data-testid={`launch-input-${input.name}`}
|
|
60
|
+
>
|
|
61
|
+
<span className="min-w-0 truncate text-[10px] font-bold text-doom-text sm:w-28 sm:shrink-0">
|
|
59
62
|
{input.name}
|
|
60
63
|
{input.required === true ? <span className="text-doom-yellow"> *</span> : null}
|
|
61
64
|
</span>
|
|
@@ -68,7 +71,7 @@ function InputField({
|
|
|
68
71
|
/>
|
|
69
72
|
) : (
|
|
70
73
|
<Select value={value === '' ? undefined : value} onValueChange={onChange}>
|
|
71
|
-
<SelectTrigger className="h-7 min-w-
|
|
74
|
+
<SelectTrigger className="h-7 w-full min-w-0 text-[10px] sm:min-w-[160px]">
|
|
72
75
|
<SelectValue placeholder="choose…" />
|
|
73
76
|
</SelectTrigger>
|
|
74
77
|
<SelectContent>
|
|
@@ -80,7 +83,7 @@ function InputField({
|
|
|
80
83
|
</SelectContent>
|
|
81
84
|
</Select>
|
|
82
85
|
)}
|
|
83
|
-
<span className="w-
|
|
86
|
+
<span className="min-w-0 truncate text-[9px] text-doom-faint sm:w-40 sm:shrink-0">{hint}</span>
|
|
84
87
|
</div>
|
|
85
88
|
);
|
|
86
89
|
}
|
|
@@ -138,10 +141,14 @@ export function LaunchWorkflowDialog({
|
|
|
138
141
|
>
|
|
139
142
|
<DialogContent width="lg" data-testid="launch-workflow-dialog" aria-describedby={undefined}>
|
|
140
143
|
<DialogHeader>
|
|
141
|
-
<div className="flex min-w-0 items-center gap-2.5">
|
|
144
|
+
<div className="flex min-w-0 flex-col items-start gap-1 sm:flex-row sm:items-center sm:gap-2.5">
|
|
142
145
|
<span className="text-[9px] text-doom-faint">launch workflow</span>
|
|
143
|
-
<DialogTitle data-testid="launch-workflow-name">
|
|
144
|
-
|
|
146
|
+
<DialogTitle data-testid="launch-workflow-name" className="max-w-full break-words">
|
|
147
|
+
{workflow.name}
|
|
148
|
+
</DialogTitle>
|
|
149
|
+
<Badge size="xs" className="max-w-full truncate">
|
|
150
|
+
{workflow.relativePath}
|
|
151
|
+
</Badge>
|
|
145
152
|
</div>
|
|
146
153
|
</DialogHeader>
|
|
147
154
|
<DialogBody>
|
|
@@ -186,7 +193,10 @@ export function LaunchWorkflowDialog({
|
|
|
186
193
|
<span className="flex h-7 items-center text-[10px] text-doom-dim">whatever the workflow resolves</span>
|
|
187
194
|
) : (
|
|
188
195
|
<Select value={runner} onValueChange={setRunner}>
|
|
189
|
-
<SelectTrigger
|
|
196
|
+
<SelectTrigger
|
|
197
|
+
data-testid="launch-runner"
|
|
198
|
+
className="h-7 w-full min-w-0 text-[10px] sm:min-w-[160px]"
|
|
199
|
+
>
|
|
190
200
|
<SelectValue />
|
|
191
201
|
</SelectTrigger>
|
|
192
202
|
<SelectContent>
|
|
@@ -226,8 +236,10 @@ export function LaunchWorkflowDialog({
|
|
|
226
236
|
{problems.join(' ')}
|
|
227
237
|
</p>
|
|
228
238
|
)}
|
|
229
|
-
<DialogFooter>
|
|
230
|
-
<span className="text-[9px] text-doom-faint">
|
|
239
|
+
<DialogFooter className="flex-wrap sm:flex-nowrap">
|
|
240
|
+
<span className="w-full text-[9px] text-doom-faint sm:w-auto">
|
|
241
|
+
the run appears on the board as soon as it starts
|
|
242
|
+
</span>
|
|
231
243
|
<span className="min-w-0 flex-1" />
|
|
232
244
|
<Button variant="outline" size="xs" data-testid="launch-cancel" onClick={onClose}>
|
|
233
245
|
cancel
|
|
@@ -176,7 +176,7 @@ export function WorkflowCatalogDrawer({
|
|
|
176
176
|
data-testid="workflow-catalog-drawer"
|
|
177
177
|
tabIndex={0}
|
|
178
178
|
onKeyDown={onKeyDown}
|
|
179
|
-
className="flex w-[440px] shrink-0 flex-col overflow-hidden border-l border-doom-border bg-doom-rail outline-none"
|
|
179
|
+
className="flex w-[min(440px,calc(100vw-24px))] shrink-0 flex-col overflow-hidden border-l border-doom-border bg-doom-rail outline-none"
|
|
180
180
|
>
|
|
181
181
|
<div className="flex h-11 shrink-0 items-center gap-2.5 border-b border-doom-border px-4">
|
|
182
182
|
<span className="text-[13px] font-bold text-doom-hi">workflow catalog</span>
|
package/web/WorkflowsPanel.tsx
CHANGED
|
@@ -147,7 +147,7 @@ function WorkflowPicker({
|
|
|
147
147
|
variant="outline"
|
|
148
148
|
size="sm"
|
|
149
149
|
data-testid="workflow-picker"
|
|
150
|
-
className="h-8 w-[360px] justify-start border-doom-border bg-doom-deep px-2.5"
|
|
150
|
+
className="h-8 w-full max-w-[360px] justify-start border-doom-border bg-doom-deep px-2.5"
|
|
151
151
|
>
|
|
152
152
|
<SearchIcon className="h-3 w-3 shrink-0 text-doom-faint" />
|
|
153
153
|
<span className="min-w-0 flex-1 truncate text-left font-bold text-doom-hi">{selected.displayName}</span>
|
|
@@ -170,9 +170,11 @@ function WorkflowPicker({
|
|
|
170
170
|
/>
|
|
171
171
|
</span>
|
|
172
172
|
</PopoverHeader>
|
|
173
|
-
<div className="grid max-h-[360px] grid-cols-
|
|
173
|
+
<div className="grid max-h-[360px] grid-cols-1 gap-2 overflow-y-auto p-2 sm:grid-cols-2">
|
|
174
174
|
{sections.length === 0 ? (
|
|
175
|
-
<span className="
|
|
175
|
+
<span className="px-2 py-5 text-center text-[10px] text-doom-faint sm:col-span-2">
|
|
176
|
+
no matching workflows
|
|
177
|
+
</span>
|
|
176
178
|
) : (
|
|
177
179
|
sections.map((section) => (
|
|
178
180
|
<div key={section.label} className="flex min-w-0 flex-col gap-0.5">
|
|
@@ -596,10 +598,10 @@ export function WorkflowsPanel({ sessionId, openTransientTab, sendSessionFrame }
|
|
|
596
598
|
</Button>
|
|
597
599
|
</div>
|
|
598
600
|
<SelectedAttention run={run} />
|
|
599
|
-
<div className="flex min-h-0 flex-1 gap-3">
|
|
601
|
+
<div className="flex min-h-0 flex-1 flex-col gap-3 sm:flex-row">
|
|
600
602
|
<div
|
|
601
603
|
data-testid="jobs-pane"
|
|
602
|
-
className="flex w-
|
|
604
|
+
className="flex max-h-44 w-full shrink-0 flex-col overflow-y-auto rounded-md border border-doom-border bg-doom-panel p-1.5 sm:max-h-none sm:w-[214px]"
|
|
603
605
|
>
|
|
604
606
|
<div className="flex items-center px-2 pb-1 pt-1">
|
|
605
607
|
<span className="text-[8px] font-bold tracking-[0.14em] text-doom-faint">JOBS</span>
|
package/web/terminalApi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sealedTransport } from '@agimon-ai/doompi-web-security/browser';
|
|
1
2
|
import {
|
|
2
3
|
WORKFLOW_SCREEN_EVENT,
|
|
3
4
|
workflowRunPath,
|
|
@@ -28,7 +29,11 @@ function errorOf(body: unknown, fallback: string): string {
|
|
|
28
29
|
|
|
29
30
|
async function post(path: string, body: unknown): Promise<{ status: number; body: unknown }> {
|
|
30
31
|
try {
|
|
31
|
-
const response = await fetch(path, {
|
|
32
|
+
const response = await sealedTransport.fetch(path, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: JSON_HEADERS,
|
|
35
|
+
body: JSON.stringify(body),
|
|
36
|
+
});
|
|
32
37
|
const text = await response.text();
|
|
33
38
|
return { status: response.status, body: text === '' ? undefined : (JSON.parse(text) as unknown) };
|
|
34
39
|
} catch {
|
|
@@ -106,7 +111,7 @@ export type DeleteWorkflowResult = { result: WorkflowDeleteResponse } | { error:
|
|
|
106
111
|
/** Permanently removes one settled run and its run directory. */
|
|
107
112
|
export async function deleteWorkflowRun(workspace: string, runKey: string): Promise<DeleteWorkflowResult> {
|
|
108
113
|
try {
|
|
109
|
-
const response = await fetch(workflowRunPath(workspace, runKey), { method: 'DELETE' });
|
|
114
|
+
const response = await sealedTransport.fetch(workflowRunPath(workspace, runKey), { method: 'DELETE' });
|
|
110
115
|
const body = (await response.json()) as unknown;
|
|
111
116
|
if (!response.ok) return { error: errorOf(body, 'The workflow could not be deleted.') };
|
|
112
117
|
if (isRecord(body) && body.deleted === true) return { result: { deleted: true } };
|
|
@@ -120,7 +125,7 @@ export type ArtifactsResult = { artifacts: WorkflowArtifactsResponse } | { error
|
|
|
120
125
|
|
|
121
126
|
export async function fetchArtifacts(workspace: string, runKey: string): Promise<ArtifactsResult> {
|
|
122
127
|
try {
|
|
123
|
-
const response = await fetch(`${workflowRunPath(workspace, runKey)}/artifacts`);
|
|
128
|
+
const response = await sealedTransport.fetch(`${workflowRunPath(workspace, runKey)}/artifacts`);
|
|
124
129
|
const body = (await response.json()) as unknown;
|
|
125
130
|
if (!response.ok) return { error: errorOf(body, 'This run has no directory to read.') };
|
|
126
131
|
return { artifacts: body as WorkflowArtifactsResponse };
|
|
@@ -139,7 +144,7 @@ export function artifactContentUrl(workspace: string, runKey: string, path: stri
|
|
|
139
144
|
|
|
140
145
|
export async function fetchArtifact(workspace: string, runKey: string, path: string): Promise<ArtifactResult> {
|
|
141
146
|
try {
|
|
142
|
-
const response = await fetch(
|
|
147
|
+
const response = await sealedTransport.fetch(
|
|
143
148
|
`${workflowRunPath(workspace, runKey)}/artifacts/${path.split('/').map(encodeURIComponent).join('/')}`,
|
|
144
149
|
);
|
|
145
150
|
const body = (await response.json()) as unknown;
|